Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10bb432a14 | |||
| e6b00afc27 | |||
| 60ebe928b0 | |||
| 2482e56fdc | |||
| 290fa837b3 | |||
| 0344163cd5 | |||
| 3a0efadebb | |||
| 8c2ffe006c | |||
| ebf82b8218 | |||
| d9a18d41e3 | |||
| c6e9cc92e8 | |||
| 3ed7b231cc | |||
| 7d26333ce0 | |||
| d9210dcaef | |||
| 0cda25e07a | |||
| be07844e38 | |||
| 5484614cc1 | |||
| 7e67edd64a | |||
| 8ce871a9f7 | |||
| 3acb61d605 | |||
| c621458704 | |||
| aaac00f69a | |||
| afea46a9e1 | |||
| fb64a2e6fe | |||
| a029490fe8 | |||
| 4a057ba83c | |||
| ef6ba710e2 | |||
| 4179ca8ada | |||
| fcbcccaeb9 | |||
| fcfee371f8 | |||
| 68a8fec4f8 | |||
| 2285d5ca0b | |||
| 537524d7fe | |||
| 3cb2bc702b | |||
| b66129b448 | |||
| 8bbeabc0dc | |||
| 5117218f86 | |||
| f80b6458a1 | |||
| 8bae5abd88 | |||
| c026d31836 | |||
| f905da4f43 | |||
| 8a786b1eae | |||
| 73c6bd5721 | |||
| 32f706ffbe | |||
| 5c1ba5015b | |||
| 5307f5d8c4 | |||
| ff52a69087 | |||
| 8d3d425089 | |||
| c0a7386ff1 | |||
| 3b9642ea85 | |||
| 52c2c2d2d7 | |||
| 48dc6a555b | |||
| d92a831743 | |||
| 19d39e4da6 | |||
| a9b2f10015 | |||
| d94a6985e2 | |||
| d2ca6218ce | |||
| 99196c32eb | |||
| c08c8d28ee | |||
| a4e8f2376d | |||
| 6fee1db7ec | |||
| ac9160ea2c | |||
| f16f234679 | |||
| 4073737008 | |||
| 03d77f0c83 | |||
| 44418a384e | |||
| b35dfd278d | |||
| 47d0fcbe77 | |||
| 6b4807bbf5 | |||
| fe158c33fb | |||
| a6c18a2eb0 | |||
| 86888b5a7a | |||
| 7be23bfd8d | |||
| 2580050353 | |||
| 81e6bd7ae0 | |||
| 16e8f4996e | |||
| e8aa3c38a8 | |||
| f929cdbdef | |||
| fb01d0eaa9 |
38
CHANGELOG.md
38
CHANGELOG.md
@ -1,5 +1,43 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.0.6
|
||||||
|
Released 14 January 2017
|
||||||
|
* Upgrade to simpleplugin 2.1.0
|
||||||
|
* Browse/Library menus (file structure vs ID3 tags)
|
||||||
|
* Added multilanguage support
|
||||||
|
|
||||||
|
## v2.0.5
|
||||||
|
Released 15 October 2016
|
||||||
|
* Fixed images when listing entries
|
||||||
|
|
||||||
|
## v2.0.4
|
||||||
|
Released 5 October 2016
|
||||||
|
* Cache (permanently) starred items so we can know everywhere if an item is starred or not without querying it
|
||||||
|
* Colorize starred items
|
||||||
|
* Hide artist in lists if it is unique
|
||||||
|
* Removed list_artist_albums()
|
||||||
|
* Include simpleplugin v2.0.1 as library and remove KODI dependency
|
||||||
|
|
||||||
|
## v2.0.3
|
||||||
|
Released 4 October 2016
|
||||||
|
* Random tracks submenu
|
||||||
|
* Download tracks
|
||||||
|
* Star tracks
|
||||||
|
* Context menu for downloading or marking items as favorite
|
||||||
|
|
||||||
|
## v2.0.2
|
||||||
|
* main code (main.py) fully rewritten
|
||||||
|
* Use SimplePlugin framework (http://romanvm.github.io/script.module.simpleplugin/index.html)
|
||||||
|
* Cache datas
|
||||||
|
|
||||||
|
## v2.0.1
|
||||||
|
* New setting 'albums_per_page'
|
||||||
|
* New menu structure
|
||||||
|
* Albums : Newest / Most played / Rencently played / by Genre
|
||||||
|
* Tracks : Starred / Random by genre / Random by year
|
||||||
|
* Upgraded: libsonic to v.0.5.0 (https://github.com/crustymonkey/py-sonic)
|
||||||
|
* Code cleanup
|
||||||
|
|
||||||
## v2.0.0
|
## v2.0.0
|
||||||
Released 14 September 2015
|
Released 14 September 2015
|
||||||
|
|
||||||
|
|||||||
27
README.md
27
README.md
@ -1,19 +1,30 @@
|
|||||||
# Subsonic for Kodi
|
# Subsonic
|
||||||
Kodi plugin to stream music from Subsonic.
|
Kodi plugin to stream, star and download music from Subsonic.
|
||||||
|
For feature requests / issues:
|
||||||
|
https://github.com/gordielachance/plugin.audio.subsonic/issues
|
||||||
|
Contributions are welcome:
|
||||||
|
https://github.com/gordielachance/plugin.audio.subsonic
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Playlist support
|
* Browse by artist, albums (newest/most played/recently played/random), tracks (starred/random), and playlists
|
||||||
* Browse by artist, album and genre
|
* Download songs
|
||||||
* Random playlists
|
* Star songs
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
* Navigate to your `.kodi/addons/` folder
|
* 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.
|
* (Re)start Kodi.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
* Scrobble to Last.FM (http://forum.kodi.tv/showthread.php?tid=195597&pid=2429362#pid2429362)
|
||||||
|
* Improve the caching system
|
||||||
|
* Search filter GUI for tracks and albums
|
||||||
|
|
||||||
## License
|
## License
|
||||||
See the `LICENSE` file.
|
See the `LICENSE` file.
|
||||||
|
|
||||||
Additional copyright notices:
|
Additional copyright notices:
|
||||||
* The original [SubKodi](https://github.com/DarkAllMan/SubKodi) plugin.
|
* [Previous version of this plugin](https://github.com/basilfx/plugin.audio.subsonic) by basilfx
|
||||||
* [`py-sonic`](https://github.com/crustymonkey/py-sonic) Python module.
|
* [SimplePlugin](https://github.com/romanvm/script.module.simpleplugin/stargazers) by romanvm
|
||||||
|
* The original [SubKodi](https://github.com/DarkAllMan/SubKodi) plugin
|
||||||
|
* [`py-sonic`](https://github.com/crustymonkey/py-sonic) Python module
|
||||||
|
|||||||
383
addon.py
383
addon.py
@ -1,383 +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("plugin.audio.subsonic")
|
|
||||||
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.random_count = addon.getSetting("random_count")
|
|
||||||
self.bitrate = addon.getSetting("bitrate")
|
|
||||||
self.transcode_format = addon.getSetting("transcode_format")
|
|
||||||
|
|
||||||
# Create connection
|
|
||||||
self.connection = libsonic_extra.SubsonicClient(
|
|
||||||
self.url, self.username, self.password)
|
|
||||||
|
|
||||||
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_page"])[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_page(self):
|
|
||||||
"""
|
|
||||||
Display main menu.
|
|
||||||
"""
|
|
||||||
|
|
||||||
menu = [
|
|
||||||
{"mode": "starred_list", "foldername": "Starred"},
|
|
||||||
{"mode": "playlists_list", "foldername": "Playlists"},
|
|
||||||
{"mode": "artist_list", "foldername": "Artists"},
|
|
||||||
{"mode": "genre_list", "foldername": "Genres"},
|
|
||||||
{"mode": "random_list", "foldername": "Random songs"}]
|
|
||||||
|
|
||||||
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 starred_list(self):
|
|
||||||
"""
|
|
||||||
Display starred songs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
xbmcplugin.setContent(self.addon_handle, "songs")
|
|
||||||
|
|
||||||
for starred in self.connection.walk_starred():
|
|
||||||
self.add_track(starred, show_artist=True)
|
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
|
||||||
|
|
||||||
def playlists_list(self):
|
|
||||||
"""
|
|
||||||
Display playlists.
|
|
||||||
"""
|
|
||||||
|
|
||||||
for playlist in self.connection.walk_playlists():
|
|
||||||
cover_art_url = self.connection.getCoverArtUrl(
|
|
||||||
playlist["coverArt"])
|
|
||||||
url = self.build_url({
|
|
||||||
"mode": "playlist_list", "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 playlist_list(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 genre_list(self):
|
|
||||||
"""
|
|
||||||
Display list of genres menu.
|
|
||||||
"""
|
|
||||||
|
|
||||||
for genre in self.connection.walk_genres():
|
|
||||||
url = self.build_url({
|
|
||||||
"mode": "albums_by_genre_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 albums_by_genre_list(self):
|
|
||||||
"""
|
|
||||||
Display album list by genre menu.
|
|
||||||
"""
|
|
||||||
|
|
||||||
genre = self.addon_args["foldername"][0].decode("utf-8")
|
|
||||||
|
|
||||||
xbmcplugin.setContent(self.addon_handle, "albums")
|
|
||||||
|
|
||||||
for album in self.connection.walk_album_list_genre(genre):
|
|
||||||
self.add_album(album, show_artist=True)
|
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
|
||||||
|
|
||||||
def artist_list(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 random_list(self):
|
|
||||||
"""
|
|
||||||
Display random options.
|
|
||||||
"""
|
|
||||||
|
|
||||||
menu = [
|
|
||||||
{"mode": "random_by_genre_list", "foldername": "By genre"},
|
|
||||||
{"mode": "random_by_year_list", "foldername": "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 random_by_genre_list(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_random_songs(
|
|
||||||
size=self.random_count, genre=genre):
|
|
||||||
self.add_track(track, show_artist=True)
|
|
||||||
|
|
||||||
xbmcplugin.endOfDirectory(self.addon_handle)
|
|
||||||
|
|
||||||
def random_by_year_list(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_random_songs(
|
|
||||||
size=self.random_count, 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()
|
|
||||||
41
addon.xml
41
addon.xml
@ -1,21 +1,44 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="plugin.audio.subsonic" name="Subsonic" version="2.0.0" provider-name="BasilFX">
|
<addon id="plugin.audio.subsonic" name="Subsonic" version="2.0.6" provider-name="BasilFX,grosbouff">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="2.19.0"/>
|
<import addon="xbmc.python" version="2.14.0"/>
|
||||||
</requires>
|
<import addon="script.module.dateutil" version="2.4.2"/>
|
||||||
<extension point="xbmc.python.pluginsource" library="addon.py">
|
</requires>
|
||||||
|
<extension point="xbmc.python.pluginsource" library="main.py">
|
||||||
<provides>audio</provides>
|
<provides>audio</provides>
|
||||||
</extension>
|
</extension>
|
||||||
<extension point="xbmc.addon.metadata">
|
<extension point="xbmc.addon.metadata">
|
||||||
<summary lang="en">Subsonic music addon for Kodi.</summary>
|
<summary lang="en">Subsonic music addon for Kodi.</summary>
|
||||||
<description lang="en">Subsonic music addon for Kodi. Stream your tunes directly to Kodi.</description>
|
<summary lang="fr">Extension Subsonic pour Kodi.</summary>
|
||||||
|
<summary lang="de">Subsonic Musik Addon für Kodi.</summary>
|
||||||
|
<description lang="en">
|
||||||
|
Stream, star and download your tunes, directly to Kodi !
|
||||||
|
For feature requests / issues:
|
||||||
|
https://github.com/gordielachance/plugin.audio.subsonic/issues
|
||||||
|
Contributions are welcome:
|
||||||
|
https://github.com/gordielachance/plugin.audio.subsonic
|
||||||
|
</description>
|
||||||
|
<description lang="fr">
|
||||||
|
Jouez, marquez vos favoris et téléchargez votre musique, directement dans Kodi !
|
||||||
|
Pour les demandes et problèmes :
|
||||||
|
https://github.com/gordielachance/plugin.audio.subsonic/issues
|
||||||
|
Les contributions sont les bienvenues :
|
||||||
|
https://github.com/gordielachance/plugin.audio.subsonic
|
||||||
|
</description>
|
||||||
|
<description lang="de">
|
||||||
|
Streame, bewerte und downloade deine Medien direkt in Kodi !
|
||||||
|
Für neue Eigentschaften oder Fehler:
|
||||||
|
https://github.com/gordielachance/plugin.audio.subsonic/issues
|
||||||
|
Beihilfe ist Willkommen:
|
||||||
|
https://github.com/gordielachance/plugin.audio.subsonic
|
||||||
|
</description>
|
||||||
<disclaimer lang="en"></disclaimer>
|
<disclaimer lang="en"></disclaimer>
|
||||||
<language>en</language>
|
<language>multi</language>
|
||||||
<platform>all</platform>
|
<platform>all</platform>
|
||||||
<license>MIT</license>
|
<license>MIT</license>
|
||||||
<forum></forum>
|
<forum></forum>
|
||||||
<website>http://www.subsonic.org</website>
|
<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>
|
<email></email>
|
||||||
</extension>
|
</extension>
|
||||||
</addon>
|
</addon>
|
||||||
|
|||||||
@ -29,4 +29,4 @@ print conn.ping()
|
|||||||
|
|
||||||
from connection import *
|
from connection import *
|
||||||
|
|
||||||
__version__ = '0.3.4'
|
__version__ = '0.6.2'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,6 @@ import urllib
|
|||||||
import urlparse
|
import urlparse
|
||||||
import libsonic
|
import libsonic
|
||||||
|
|
||||||
|
|
||||||
def force_list(value):
|
def force_list(value):
|
||||||
"""
|
"""
|
||||||
Coerce the input value to a list.
|
Coerce the input value to a list.
|
||||||
@ -34,7 +33,7 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
- Add conventient `walk_*' methods to iterate over the API responses.
|
- Add conventient `walk_*' methods to iterate over the API responses.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, url, username, password):
|
def __init__(self, url, username, password, apiversion, insecure, legacyauth):
|
||||||
"""
|
"""
|
||||||
Construct a new SubsonicClient.
|
Construct a new SubsonicClient.
|
||||||
|
|
||||||
@ -64,7 +63,7 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
|
|
||||||
# Invoke original constructor
|
# Invoke original constructor
|
||||||
super(SubsonicClient, self).__init__(
|
super(SubsonicClient, self).__init__(
|
||||||
host, username, password, port=port)
|
host, username, password, port=port, appName='Kodi', apiVersion=apiversion, insecure=insecure, legacyAuth=legacyauth)
|
||||||
|
|
||||||
def getIndexes(self, *args, **kwargs):
|
def getIndexes(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -141,6 +140,7 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
|
|
||||||
def getArtists(self, *args, **kwargs):
|
def getArtists(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
(ID3 tags)
|
||||||
Improve the getArtists method. Ensures IDs are integers.
|
Improve the getArtists method. Ensures IDs are integers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -163,6 +163,7 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
|
|
||||||
def getArtist(self, *args, **kwargs):
|
def getArtist(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
(ID3 tags)
|
||||||
Improve the getArtist method. Ensures IDs are integers.
|
Improve the getArtist method. Ensures IDs are integers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -210,6 +211,7 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
|
|
||||||
def getAlbum(self, *args, **kwargs):
|
def getAlbum(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
(ID3 tags)
|
||||||
Improve the getAlbum method. Ensures the IDs are real integers.
|
Improve the getAlbum method. Ensures the IDs are real integers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -297,24 +299,17 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
else:
|
else:
|
||||||
return super(SubsonicClient, self)._doBinReq(*args, **kwargs)
|
return super(SubsonicClient, self)._doBinReq(*args, **kwargs)
|
||||||
|
|
||||||
def walk_index(self):
|
def walk_index(self, folder_id=None):
|
||||||
"""
|
"""
|
||||||
Request Subsonic's index and iterate each item.
|
Request Subsonic's index and iterate each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = self.getIndexes()
|
response = self.getIndexes(folder_id)
|
||||||
|
|
||||||
for index in response["indexes"]["index"]:
|
for index in response["indexes"]["index"]:
|
||||||
for index in index["artist"]:
|
for artist in index["artist"]:
|
||||||
for item in self.walk_directory(index["id"]):
|
yield artist
|
||||||
yield item
|
|
||||||
|
|
||||||
for child in response["indexes"]["child"]:
|
|
||||||
if child.get("isDir"):
|
|
||||||
for child in self.walk_directory(child["id"]):
|
|
||||||
yield child
|
|
||||||
else:
|
|
||||||
yield child
|
|
||||||
|
|
||||||
def walk_playlists(self):
|
def walk_playlists(self):
|
||||||
"""
|
"""
|
||||||
@ -336,15 +331,11 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
for child in response["playlist"]["entry"]:
|
for child in response["playlist"]["entry"]:
|
||||||
yield child
|
yield child
|
||||||
|
|
||||||
def walk_starred(self):
|
def walk_folders(self):
|
||||||
"""
|
response = self.getMusicFolders()
|
||||||
Request Subsonic's starred songs and iterate over each item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
response = self.getStarred()
|
for child in response["musicFolders"]["musicFolder"]:
|
||||||
|
yield child
|
||||||
for song in response["starred"]["song"]:
|
|
||||||
yield song
|
|
||||||
|
|
||||||
def walk_directory(self, directory_id):
|
def walk_directory(self, directory_id):
|
||||||
"""
|
"""
|
||||||
@ -372,6 +363,7 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
|
|
||||||
def walk_artists(self):
|
def walk_artists(self):
|
||||||
"""
|
"""
|
||||||
|
(ID3 tags)
|
||||||
Request all artists and iterate over each item.
|
Request all artists and iterate over each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -383,6 +375,7 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
|
|
||||||
def walk_genres(self):
|
def walk_genres(self):
|
||||||
"""
|
"""
|
||||||
|
(ID3 tags)
|
||||||
Request all genres and iterate over each item.
|
Request all genres and iterate over each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -391,28 +384,32 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
for genre in response["genres"]["genre"]:
|
for genre in response["genres"]["genre"]:
|
||||||
yield genre
|
yield genre
|
||||||
|
|
||||||
def walk_album_list_genre(self, genre):
|
def walk_albums(self, 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.
|
Request all albums for a given genre and iterate over each album.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
offset = 0
|
if ltype == 'byGenre' and genre is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if ltype == 'byYear' and (fromYear is None or toYear is None):
|
||||||
|
return
|
||||||
|
|
||||||
while True:
|
|
||||||
response = self.getAlbumList2(
|
response = self.getAlbumList2(
|
||||||
ltype="byGenre", genre=genre, size=500, offset=offset)
|
ltype=ltype, size=size, fromYear=fromYear, toYear=toYear,genre=genre, offset=offset)
|
||||||
|
|
||||||
if not response["albumList2"]["album"]:
|
if not response["albumList2"]["album"]:
|
||||||
break
|
return
|
||||||
|
|
||||||
for album in response["albumList2"]["album"]:
|
for album in response["albumList2"]["album"]:
|
||||||
yield album
|
yield album
|
||||||
|
|
||||||
offset += 500
|
|
||||||
|
|
||||||
def walk_album(self, album_id):
|
def walk_album(self, album_id):
|
||||||
"""
|
"""
|
||||||
Request an alum and iterate over each item.
|
(ID3 tags)
|
||||||
|
Request an album and iterate over each item.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = self.getAlbum(album_id)
|
response = self.getAlbum(album_id)
|
||||||
@ -420,14 +417,24 @@ class SubsonicClient(libsonic.Connection):
|
|||||||
for song in response["album"]["song"]:
|
for song in response["album"]["song"]:
|
||||||
yield song
|
yield song
|
||||||
|
|
||||||
def walk_random_songs(self, size, genre=None, from_year=None,
|
def walk_tracks_random(self, size=None, genre=None, fromYear=None,toYear=None):
|
||||||
to_year=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 = self.getRandomSongs(
|
response = self.getRandomSongs(
|
||||||
size=size, genre=genre, fromYear=from_year, toYear=to_year)
|
size=size, genre=genre, fromYear=fromYear, toYear=toYear)
|
||||||
|
|
||||||
for song in response["randomSongs"]["song"]:
|
for song in response["randomSongs"]["song"]:
|
||||||
yield song
|
yield song
|
||||||
|
|
||||||
|
|
||||||
|
def walk_tracks_starred(self):
|
||||||
|
"""
|
||||||
|
Request Subsonic's starred songs and iterate over each item.
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.getStarred()
|
||||||
|
|
||||||
|
for song in response["starred"]["song"]:
|
||||||
|
yield song
|
||||||
|
|||||||
4
lib/simpleplugin/__init__.py
Normal file
4
lib/simpleplugin/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#v2.1.0
|
||||||
|
#https://github.com/romanvm/script.module.simpleplugin/releases
|
||||||
|
|
||||||
|
from simpleplugin import *
|
||||||
923
lib/simpleplugin/simpleplugin.py
Normal file
923
lib/simpleplugin/simpleplugin.py
Normal file
@ -0,0 +1,923 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Created on: 03.06.2015
|
||||||
|
"""
|
||||||
|
SimplePlugin micro-framework for Kodi content plugins
|
||||||
|
|
||||||
|
**Author**: Roman Miroshnychenko aka Roman V.M.
|
||||||
|
|
||||||
|
**License**: `GPL v.3 <https://www.gnu.org/copyleft/gpl.html>`_
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import cPickle as pickle
|
||||||
|
from urlparse import parse_qs
|
||||||
|
from urllib import urlencode
|
||||||
|
from functools import wraps
|
||||||
|
from collections import MutableMapping, namedtuple
|
||||||
|
from copy import deepcopy
|
||||||
|
from types import GeneratorType
|
||||||
|
from hashlib import md5
|
||||||
|
from shutil import move
|
||||||
|
import xbmcaddon
|
||||||
|
import xbmc
|
||||||
|
import xbmcplugin
|
||||||
|
import xbmcgui
|
||||||
|
|
||||||
|
__all__ = ['SimplePluginError', 'Storage', 'Addon', 'Plugin', 'Params']
|
||||||
|
|
||||||
|
ListContext = namedtuple('ListContext', ['listing', 'succeeded', 'update_listing', 'cache_to_disk',
|
||||||
|
'sort_methods', 'view_mode', 'content'])
|
||||||
|
PlayContext = namedtuple('PlayContext', ['path', 'play_item', 'succeeded'])
|
||||||
|
|
||||||
|
|
||||||
|
class SimplePluginError(Exception):
|
||||||
|
"""Custom exception"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Params(dict):
|
||||||
|
"""
|
||||||
|
Params(**kwargs)
|
||||||
|
|
||||||
|
A class that stores parsed plugin call parameters
|
||||||
|
|
||||||
|
Parameters can be accessed both through :class:`dict` keys and
|
||||||
|
instance properties.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@plugin.action('foo')
|
||||||
|
def action(params):
|
||||||
|
foo = params['foo'] # Access by key
|
||||||
|
bar = params.bar # Access through property. Both variants are equal
|
||||||
|
"""
|
||||||
|
def __getattr__(self, item):
|
||||||
|
if item not in self:
|
||||||
|
raise AttributeError('Invalid parameter: "{0}"!'.format(item))
|
||||||
|
return self[item]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '<Params {0}>'.format(super(Params, self).__repr__())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<simpleplugin.Params object {0}>'.format(super(Params, self).__repr__())
|
||||||
|
|
||||||
|
|
||||||
|
class Storage(MutableMapping):
|
||||||
|
"""
|
||||||
|
Persistent storage for arbitrary data with a dictionary-like interface
|
||||||
|
|
||||||
|
It is designed as a context manager and better be used
|
||||||
|
with 'with' statement.
|
||||||
|
|
||||||
|
:param storage_dir: directory for storage
|
||||||
|
:type storage_dir: str
|
||||||
|
:param filename: the name of a storage file (optional)
|
||||||
|
:type filename: str
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
with Storage('/foo/bar/storage/') as storage:
|
||||||
|
storage['key1'] = value1
|
||||||
|
value2 = storage['key2']
|
||||||
|
|
||||||
|
.. note:: After exiting :keyword:`with` block a :class:`Storage` instance is invalidated.
|
||||||
|
Storage contents are saved to disk only for a new storage or if the contents have been changed.
|
||||||
|
"""
|
||||||
|
def __init__(self, storage_dir, filename='storage.pcl'):
|
||||||
|
"""
|
||||||
|
Class constructor
|
||||||
|
"""
|
||||||
|
self._storage = {}
|
||||||
|
self._hash = None
|
||||||
|
self._filename = os.path.join(storage_dir, filename)
|
||||||
|
try:
|
||||||
|
with open(self._filename, 'rb') as fo:
|
||||||
|
contents = fo.read()
|
||||||
|
self._storage = pickle.loads(contents)
|
||||||
|
self._hash = md5(contents).hexdigest()
|
||||||
|
except (IOError, pickle.PickleError, EOFError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._storage[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._storage[key] = value
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
del self._storage[key]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._storage.__iter__()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._storage)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '<Storage {0}>'.format(self._storage)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<simpleplugin.Storage object {0}>'.format(self._storage)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""
|
||||||
|
Save storage contents to disk
|
||||||
|
|
||||||
|
This method saves new and changed :class:`Storage` contents to disk
|
||||||
|
and invalidates the Storage instance. Unchanged Storage is not saved
|
||||||
|
but simply invalidated.
|
||||||
|
"""
|
||||||
|
contents = pickle.dumps(self._storage)
|
||||||
|
if self._hash is None or md5(contents).hexdigest() != self._hash:
|
||||||
|
tmp = self._filename + '.tmp'
|
||||||
|
try:
|
||||||
|
with open(tmp, 'wb') as fo:
|
||||||
|
fo.write(contents)
|
||||||
|
except:
|
||||||
|
os.remove(tmp)
|
||||||
|
raise
|
||||||
|
move(tmp, self._filename) # Atomic save
|
||||||
|
del self._storage
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
"""
|
||||||
|
Make a copy of storage contents
|
||||||
|
|
||||||
|
.. note:: this method performs a *deep* copy operation.
|
||||||
|
|
||||||
|
:return: a copy of storage contents
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
return deepcopy(self._storage)
|
||||||
|
|
||||||
|
|
||||||
|
class Addon(object):
|
||||||
|
"""
|
||||||
|
Base addon class
|
||||||
|
|
||||||
|
Provides access to basic addon parameters
|
||||||
|
|
||||||
|
:param id_: addon id, e.g. 'plugin.video.foo' (optional)
|
||||||
|
:type id_: str
|
||||||
|
"""
|
||||||
|
def __init__(self, id_=''):
|
||||||
|
"""
|
||||||
|
Class constructor
|
||||||
|
"""
|
||||||
|
self._addon = xbmcaddon.Addon(id_)
|
||||||
|
self._configdir = xbmc.translatePath(self._addon.getAddonInfo('profile')).decode('utf-8')
|
||||||
|
self._ui_strings_map = None
|
||||||
|
if not os.path.exists(self._configdir):
|
||||||
|
os.mkdir(self._configdir)
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
"""
|
||||||
|
Get addon setting as an Addon instance attribute
|
||||||
|
|
||||||
|
E.g. addon.my_setting is equal to addon.get_setting('my_setting')
|
||||||
|
|
||||||
|
:param item:
|
||||||
|
:type item: str
|
||||||
|
"""
|
||||||
|
return self.get_setting(item)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '<Addon [{0}]>'.format(self.id)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<simpleplugin.Addon object [{0}]>'.format(self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def addon(self):
|
||||||
|
"""
|
||||||
|
Kodi Addon instance that represents this Addon
|
||||||
|
|
||||||
|
:return: Addon instance
|
||||||
|
:rtype: xbmcaddon.Addon
|
||||||
|
"""
|
||||||
|
return self._addon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
"""
|
||||||
|
Addon ID
|
||||||
|
|
||||||
|
:return: Addon ID, e.g. 'plugin.video.foo'
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return self._addon.getAddonInfo('id')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
"""
|
||||||
|
Addon path
|
||||||
|
|
||||||
|
:return: path to the addon folder
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return self._addon.getAddonInfo('path').decode('utf-8')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""
|
||||||
|
Addon icon
|
||||||
|
|
||||||
|
:return: path to the addon icon image
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
icon = os.path.join(self.path, 'icon.png')
|
||||||
|
if os.path.exists(icon):
|
||||||
|
return icon
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fanart(self):
|
||||||
|
"""
|
||||||
|
Addon fanart
|
||||||
|
|
||||||
|
:return: path to the addon fanart image
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
fanart = os.path.join(self.path, 'fanart.jpg')
|
||||||
|
if os.path.exists(fanart):
|
||||||
|
return fanart
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config_dir(self):
|
||||||
|
"""
|
||||||
|
Addon config dir
|
||||||
|
|
||||||
|
:return: path to the addon config dir
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return self._configdir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
"""
|
||||||
|
Addon version
|
||||||
|
|
||||||
|
:return: addon version
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return self._addon.getAddonInfo('version')
|
||||||
|
|
||||||
|
def get_localized_string(self, id_):
|
||||||
|
"""
|
||||||
|
Get localized UI string
|
||||||
|
|
||||||
|
:param id_: UI string ID
|
||||||
|
:type id_: int
|
||||||
|
:return: UI string in the current language
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
return self._addon.getLocalizedString(id_).encode('utf-8')
|
||||||
|
|
||||||
|
def get_setting(self, id_, convert=True):
|
||||||
|
"""
|
||||||
|
Get addon setting
|
||||||
|
|
||||||
|
If ``convert=True``, 'bool' settings are converted to Python :class:`bool` values,
|
||||||
|
and numeric strings to Python :class:`long` or :class:`float` depending on their format.
|
||||||
|
|
||||||
|
.. note:: Settings can also be read via :class:`Addon` instance poperties named as the respective settings.
|
||||||
|
I.e. ``addon.foo`` is equal to ``addon.get_setting('foo')``.
|
||||||
|
|
||||||
|
:param id_: setting ID
|
||||||
|
:type id_: str
|
||||||
|
:param convert: try to guess and convert the setting to an appropriate type
|
||||||
|
E.g. ``'1.0'`` will be converted to float ``1.0`` number, ``'true'`` to ``True`` and so on.
|
||||||
|
:type convert: bool
|
||||||
|
:return: setting value
|
||||||
|
"""
|
||||||
|
setting = self._addon.getSetting(id_)
|
||||||
|
if convert:
|
||||||
|
if setting == 'true':
|
||||||
|
return True # Convert boolean strings to bool
|
||||||
|
elif setting == 'false':
|
||||||
|
return False
|
||||||
|
elif re.search(r'^-?\d+$', setting) is not None:
|
||||||
|
return long(setting) # Convert numeric strings to long
|
||||||
|
elif re.search(r'^-?\d+\.\d+$', setting) is not None:
|
||||||
|
return float(setting) # Convert numeric strings with a dot to float
|
||||||
|
return setting
|
||||||
|
|
||||||
|
def set_setting(self, id_, value):
|
||||||
|
"""
|
||||||
|
Set addon setting
|
||||||
|
|
||||||
|
Python :class:`bool` type are converted to ``'true'`` or ``'false'``
|
||||||
|
Non-string/non-unicode values are converted to strings.
|
||||||
|
|
||||||
|
.. warning:: Setting values via :class:`Addon` instance properties is not supported!
|
||||||
|
Values can only be set using :meth:`Addon.set_setting` method.
|
||||||
|
|
||||||
|
:param id_: setting ID
|
||||||
|
:type id_: str
|
||||||
|
:param value: setting value
|
||||||
|
"""
|
||||||
|
if isinstance(value, bool):
|
||||||
|
value = 'true' if value else 'false'
|
||||||
|
elif not isinstance(value, basestring):
|
||||||
|
value = str(value)
|
||||||
|
self._addon.setSetting(id_, value)
|
||||||
|
|
||||||
|
def log(self, message, level=xbmc.LOGDEBUG):
|
||||||
|
"""
|
||||||
|
Add message to Kodi log starting with Addon ID
|
||||||
|
|
||||||
|
:param message: message to be written into the Kodi log
|
||||||
|
:type message: str
|
||||||
|
:param level: log level. :mod:`xbmc` module provides the necessary symbolic constants.
|
||||||
|
Default: ``xbmc.LOGDEBUG``
|
||||||
|
:type level: int
|
||||||
|
"""
|
||||||
|
if isinstance(message, unicode):
|
||||||
|
message = message.encode('utf-8')
|
||||||
|
xbmc.log('{0} [v.{1}]: {2}'.format(self.id, self.version, message), level)
|
||||||
|
|
||||||
|
def log_notice(self, message):
|
||||||
|
"""
|
||||||
|
Add NOTICE message to the Kodi log
|
||||||
|
|
||||||
|
:param message: message to write to the Kodi log
|
||||||
|
:type message: str
|
||||||
|
"""
|
||||||
|
self.log(message, xbmc.LOGINFO)
|
||||||
|
|
||||||
|
def log_warning(self, message):
|
||||||
|
"""
|
||||||
|
Add WARNING message to the Kodi log
|
||||||
|
|
||||||
|
:param message: message to write to the Kodi log
|
||||||
|
:type message: str
|
||||||
|
"""
|
||||||
|
self.log(message, xbmc.LOGWARNING)
|
||||||
|
|
||||||
|
def log_error(self, message):
|
||||||
|
"""
|
||||||
|
Add ERROR message to the Kodi log
|
||||||
|
|
||||||
|
:param message: message to write to the Kodi log
|
||||||
|
:type message: str
|
||||||
|
"""
|
||||||
|
self.log(message, xbmc.LOGERROR)
|
||||||
|
|
||||||
|
def log_debug(self, message):
|
||||||
|
"""
|
||||||
|
Add debug message to the Kodi log
|
||||||
|
|
||||||
|
:param message: message to write to the Kodi log
|
||||||
|
:type message: str
|
||||||
|
"""
|
||||||
|
self.log(message, xbmc.LOGDEBUG)
|
||||||
|
|
||||||
|
def get_storage(self, filename='storage.pcl'):
|
||||||
|
"""
|
||||||
|
Get a persistent :class:`Storage` instance for storing arbitrary values between addon calls.
|
||||||
|
|
||||||
|
A :class:`Storage` instance can be used as a context manager.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
with plugin.get_storage() as storage:
|
||||||
|
storage['param1'] = value1
|
||||||
|
value2 = storage['param2']
|
||||||
|
|
||||||
|
.. note:: After exiting :keyword:`with` block a :class:`Storage` instance is invalidated.
|
||||||
|
|
||||||
|
:param filename: the name of a storage file (optional)
|
||||||
|
:type filename: str
|
||||||
|
:return: Storage object
|
||||||
|
:rtype: Storage
|
||||||
|
"""
|
||||||
|
return Storage(self.config_dir, filename)
|
||||||
|
|
||||||
|
def cached(self, duration=10):
|
||||||
|
"""
|
||||||
|
Cached decorator
|
||||||
|
|
||||||
|
Used to cache function return data
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
@plugin.cached(30)
|
||||||
|
def my_func(*args, **kwargs):
|
||||||
|
# Do some stuff
|
||||||
|
return value
|
||||||
|
|
||||||
|
:param duration: caching duration in min (positive values only)
|
||||||
|
:type duration: int
|
||||||
|
:raises ValueError: if duration is zero or negative
|
||||||
|
"""
|
||||||
|
def outer_wrapper(func):
|
||||||
|
@wraps(func)
|
||||||
|
def inner_wrapper(*args, **kwargs):
|
||||||
|
with self.get_storage('__cache__.pcl') as cache:
|
||||||
|
current_time = datetime.now()
|
||||||
|
key = func.__name__ + str(args) + str(kwargs)
|
||||||
|
try:
|
||||||
|
data, timestamp = cache[key]
|
||||||
|
if duration > 0 and current_time - timestamp > timedelta(minutes=duration):
|
||||||
|
raise KeyError
|
||||||
|
elif duration <= 0:
|
||||||
|
raise ValueError('Caching duration cannot be zero or negative!')
|
||||||
|
self.log_debug('Cache hit: {0}'.format(key))
|
||||||
|
except KeyError:
|
||||||
|
self.log_debug('Cache miss: {0}'.format(key))
|
||||||
|
data = func(*args, **kwargs)
|
||||||
|
cache[key] = (data, current_time)
|
||||||
|
return data
|
||||||
|
return inner_wrapper
|
||||||
|
return outer_wrapper
|
||||||
|
|
||||||
|
def gettext(self, ui_string):
|
||||||
|
"""
|
||||||
|
Get a translated UI string from addon localization files.
|
||||||
|
|
||||||
|
This function emulates GNU Gettext for more convenient access
|
||||||
|
to localized addon UI strings. To reduce typing this method object
|
||||||
|
can be assigned to a ``_`` (single underscore) variable.
|
||||||
|
|
||||||
|
For using gettext emulation :meth:`Addon.initialize_gettext` method
|
||||||
|
needs to be called first. See documentation for that method for more info
|
||||||
|
about Gettext emulation.
|
||||||
|
|
||||||
|
:param ui_string: a UI string from English :file:`strings.po`.
|
||||||
|
:type ui_string: str
|
||||||
|
:return: a UI string from translated :file:`strings.po`.
|
||||||
|
:rtype: unicode
|
||||||
|
:raises simpleplugin.SimplePluginError: if :meth:`Addon.initialize_gettext` wasn't called first
|
||||||
|
or if a string is not found in English :file:`strings.po`.
|
||||||
|
"""
|
||||||
|
if self._ui_strings_map is not None:
|
||||||
|
try:
|
||||||
|
return self.get_localized_string(self._ui_strings_map['strings'][ui_string])
|
||||||
|
except KeyError:
|
||||||
|
raise SimplePluginError('UI string "{0}" is not found in strings.po!'.format(ui_string))
|
||||||
|
else:
|
||||||
|
raise SimplePluginError('Addon localization is not initialized!')
|
||||||
|
|
||||||
|
def initialize_gettext(self):
|
||||||
|
"""
|
||||||
|
Initialize GNU gettext emulation in addon
|
||||||
|
|
||||||
|
Kodi localization system for addons is not very convenient
|
||||||
|
because you need to operate with numeric string codes instead
|
||||||
|
of UI strings themselves which reduces code readability and
|
||||||
|
may lead to errors. The :class:`Addon` class provides facilities
|
||||||
|
for emulating GNU Gettext localization system.
|
||||||
|
|
||||||
|
This allows to use UI strings from addon's English :file:`strings.po`
|
||||||
|
file instead of numeric codes to return localized strings from
|
||||||
|
respective localized :file:`.po` files.
|
||||||
|
|
||||||
|
This method returns :meth:`Addon.gettext` method object that
|
||||||
|
can be assigned to a short alias to reduce typing. Traditionally,
|
||||||
|
``_`` (a single underscore) is used for this purpose.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
addon = simpleplugin.Addon()
|
||||||
|
_ = addon.initialize_gettext()
|
||||||
|
|
||||||
|
xbmcgui.Dialog().notification(_('Warning!'), _('Something happened'))
|
||||||
|
|
||||||
|
In the preceding example the notification strings will be replaced
|
||||||
|
with localized versions if these strings are translated.
|
||||||
|
|
||||||
|
:return: :meth:`Addon.gettext` method object
|
||||||
|
:raises simpleplugin.SimplePluginError: if the addon's English :file:`strings.po` file is missing
|
||||||
|
"""
|
||||||
|
strings_po = os.path.join(self.path, 'resources', 'language', 'English', 'strings.po')
|
||||||
|
if os.path.exists(strings_po):
|
||||||
|
with open(strings_po, 'rb') as fo:
|
||||||
|
raw_strings = fo.read()
|
||||||
|
raw_strings_hash = md5(raw_strings).hexdigest()
|
||||||
|
gettext_pcl = '__gettext__.pcl'
|
||||||
|
with self.get_storage(gettext_pcl) as ui_strings_map:
|
||||||
|
if (not os.path.exists(os.path.join(self._configdir, gettext_pcl)) or
|
||||||
|
raw_strings_hash != ui_strings_map['hash']):
|
||||||
|
ui_strings = self._parse_po(raw_strings.split('\n'))
|
||||||
|
self._ui_strings_map = {
|
||||||
|
'hash': raw_strings_hash,
|
||||||
|
'strings': ui_strings
|
||||||
|
}
|
||||||
|
ui_strings_map['hash'] = raw_strings_hash
|
||||||
|
ui_strings_map['strings'] = ui_strings.copy()
|
||||||
|
else:
|
||||||
|
self._ui_strings_map = deepcopy(ui_strings_map)
|
||||||
|
else:
|
||||||
|
raise SimplePluginError('Unable to initialize localization because of missing English strings.po!')
|
||||||
|
return self.gettext
|
||||||
|
|
||||||
|
def _parse_po(self, strings):
|
||||||
|
"""
|
||||||
|
Parses ``strings.po`` file into a dict of {'string': id} items.
|
||||||
|
"""
|
||||||
|
ui_strings = {}
|
||||||
|
string_id = None
|
||||||
|
for string in strings:
|
||||||
|
if string_id is None and 'msgctxt' in string:
|
||||||
|
string_id = int(re.search(r'"#(\d+)"', string).group(1))
|
||||||
|
elif string_id is not None and 'msgid' in string:
|
||||||
|
ui_strings[re.search(r'"(.*?)"', string, re.U).group(1)] = string_id
|
||||||
|
string_id = None
|
||||||
|
return ui_strings
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(Addon):
|
||||||
|
"""
|
||||||
|
Plugin class
|
||||||
|
|
||||||
|
:param id_: plugin's id, e.g. 'plugin.video.foo' (optional)
|
||||||
|
:type id_: str
|
||||||
|
|
||||||
|
This class provides a simplified API to create virtual directories of playable items
|
||||||
|
for Kodi content plugins.
|
||||||
|
:class:`simpleplugin.Plugin` uses a concept of callable plugin actions (functions or methods)
|
||||||
|
that are defined using :meth:`Plugin.action` decorator.
|
||||||
|
A Plugin instance must have at least one action that is named ``'root'``.
|
||||||
|
|
||||||
|
Minimal example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from simpleplugin import Plugin
|
||||||
|
|
||||||
|
plugin = Plugin()
|
||||||
|
|
||||||
|
@plugin.action()
|
||||||
|
def root(params): # Mandatory item!
|
||||||
|
return [{'label': 'Foo',
|
||||||
|
'url': plugin.get_url(action='some_action', param='Foo')},
|
||||||
|
{'label': 'Bar',
|
||||||
|
'url': plugin.get_url(action='some_action', param='Bar')}]
|
||||||
|
|
||||||
|
@plugin.action()
|
||||||
|
def some_action(params):
|
||||||
|
return [{'label': params['param']}]
|
||||||
|
|
||||||
|
plugin.run()
|
||||||
|
|
||||||
|
An action callable receives 1 parameter -- params.
|
||||||
|
params is a dict-like object containing plugin call parameters (including action string)
|
||||||
|
The action callable can return
|
||||||
|
either a list/generator of dictionaries representing Kodi virtual directory items
|
||||||
|
or a resolved playable path (:class:`str` or :obj:`unicode`) for Kodi to play.
|
||||||
|
|
||||||
|
Example 1::
|
||||||
|
|
||||||
|
@plugin.action()
|
||||||
|
def list_action(params):
|
||||||
|
listing = get_listing(params) # Some external function to create listing
|
||||||
|
return listing
|
||||||
|
|
||||||
|
The ``listing`` variable is a Python list/generator of dict items.
|
||||||
|
Example 2::
|
||||||
|
|
||||||
|
@plugin.action()
|
||||||
|
def play_action(params):
|
||||||
|
path = get_path(params) # Some external function to get a playable path
|
||||||
|
return path
|
||||||
|
|
||||||
|
Each dict item can contain the following properties:
|
||||||
|
|
||||||
|
- label -- item's label (default: ``''``).
|
||||||
|
- label2 -- item's label2 (default: ``''``).
|
||||||
|
- thumb -- item's thumbnail (default: ``''``).
|
||||||
|
- icon -- item's icon (default: ``''``).
|
||||||
|
- path -- item's path (default: ``''``).
|
||||||
|
- fanart -- item's fanart (optional).
|
||||||
|
- art -- a dict containing all item's graphic (see :meth:`xbmcgui.ListItem.setArt` for more info) -- optional.
|
||||||
|
- stream_info -- a dictionary of ``{stream_type: {param: value}}`` items
|
||||||
|
(see :meth:`xbmcgui.ListItem.addStreamInfo`) -- optional.
|
||||||
|
- info -- a dictionary of ``{media: {param: value}}`` items
|
||||||
|
(see :meth:`xbmcgui.ListItem.setInfo`) -- optional
|
||||||
|
- context_menu - a list that contains 2-item tuples ``('Menu label', 'Action')``.
|
||||||
|
The items from the tuples are added to the item's context menu.
|
||||||
|
- url -- a callback URL for this list item.
|
||||||
|
- is_playable -- if ``True``, then this item is playable and must return a playable path or
|
||||||
|
be resolved via :meth:`Plugin.resolve_url` (default: ``False``).
|
||||||
|
- is_folder -- if ``True`` then the item will open a lower-level sub-listing. if ``False``,
|
||||||
|
the item either is a playable media or a general-purpose script
|
||||||
|
which neither creates a virtual folder nor points to a playable media (default: C{True}).
|
||||||
|
if ``'is_playable'`` is set to ``True``, then ``'is_folder'`` value automatically assumed to be ``False``.
|
||||||
|
- subtitles -- the list of paths to subtitle files (optional).
|
||||||
|
- mime -- item's mime type (optional).
|
||||||
|
- list_item -- an 'class:`xbmcgui.ListItem` instance (optional).
|
||||||
|
It is used when you want to set all list item properties by yourself.
|
||||||
|
If ``'list_item'`` property is present, all other properties,
|
||||||
|
except for ``'url'`` and ``'is_folder'``, are ignored.
|
||||||
|
- properties -- a dictionary of list item properties
|
||||||
|
(see :meth:`xbmcgui.ListItem.setProperty`) -- optional.
|
||||||
|
|
||||||
|
Example 3::
|
||||||
|
|
||||||
|
listing = [{ 'label': 'Label',
|
||||||
|
'label2': 'Label 2',
|
||||||
|
'thumb': 'thumb.png',
|
||||||
|
'icon': 'icon.png',
|
||||||
|
'fanart': 'fanart.jpg',
|
||||||
|
'art': {'clearart': 'clearart.png'},
|
||||||
|
'stream_info': {'video': {'codec': 'h264', 'duration': 1200},
|
||||||
|
'audio': {'codec': 'ac3', 'language': 'en'}},
|
||||||
|
'info': {'video': {'genre': 'Comedy', 'year': 2005}},
|
||||||
|
'context_menu': [('Menu Item', 'Action')],
|
||||||
|
'url': 'plugin:/plugin.video.test/?action=play',
|
||||||
|
'is_playable': True,
|
||||||
|
'is_folder': False,
|
||||||
|
'subtitles': ['/path/to/subtitles.en.srt', '/path/to/subtitles.uk.srt'],
|
||||||
|
'mime': 'video/mp4'
|
||||||
|
}]
|
||||||
|
|
||||||
|
Alternatively, an action callable can use :meth:`Plugin.create_listing` and :meth:`Plugin.resolve_url`
|
||||||
|
static methods to pass additional parameters to Kodi.
|
||||||
|
|
||||||
|
Example 4::
|
||||||
|
|
||||||
|
@plugin.action()
|
||||||
|
def list_action(params):
|
||||||
|
listing = get_listing(params) # Some external function to create listing
|
||||||
|
return Plugin.create_listing(listing, sort_methods=(2, 10, 17), view_mode=500)
|
||||||
|
|
||||||
|
Example 5::
|
||||||
|
|
||||||
|
@plugin.action()
|
||||||
|
def play_action(params):
|
||||||
|
path = get_path(params) # Some external function to get a playable path
|
||||||
|
return Plugin.resolve_url(path, succeeded=True)
|
||||||
|
|
||||||
|
If an action callable performs any actions other than creating a listing or
|
||||||
|
resolving a playable URL, it must return ``None``.
|
||||||
|
"""
|
||||||
|
def __init__(self, id_=''):
|
||||||
|
"""
|
||||||
|
Class constructor
|
||||||
|
"""
|
||||||
|
super(Plugin, self).__init__(id_)
|
||||||
|
self._url = 'plugin://{0}/'.format(self.id)
|
||||||
|
self._handle = None
|
||||||
|
self.actions = {}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '<Plugin {0}>'.format(sys.argv)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<simpleplugin.Plugin object {0}>'.format(sys.argv)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_params(paramstring):
|
||||||
|
"""
|
||||||
|
Convert a URL-encoded paramstring to a Python dict
|
||||||
|
|
||||||
|
:param paramstring: URL-encoded paramstring
|
||||||
|
:type paramstring: str
|
||||||
|
:return: parsed paramstring
|
||||||
|
:rtype: Params
|
||||||
|
"""
|
||||||
|
raw_params = parse_qs(paramstring)
|
||||||
|
params = Params()
|
||||||
|
for key, value in raw_params.iteritems():
|
||||||
|
params[key] = value[0] if len(value) == 1 else value
|
||||||
|
return params
|
||||||
|
|
||||||
|
def get_url(self, plugin_url='', **kwargs):
|
||||||
|
"""
|
||||||
|
Construct a callable URL for a virtual directory item
|
||||||
|
|
||||||
|
If plugin_url is empty, a current plugin URL is used.
|
||||||
|
kwargs are converted to a URL-encoded string of plugin call parameters
|
||||||
|
To call a plugin action, 'action' parameter must be used,
|
||||||
|
if 'action' parameter is missing, then the plugin root action is called
|
||||||
|
If the action is not added to :class:`Plugin` actions, :class:`PluginError` will be raised.
|
||||||
|
|
||||||
|
:param plugin_url: plugin URL with trailing / (optional)
|
||||||
|
:type plugin_url: str
|
||||||
|
:param kwargs: pairs of key=value items
|
||||||
|
:return: a full plugin callback URL
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
url = plugin_url or self._url
|
||||||
|
if kwargs:
|
||||||
|
return '{0}?{1}'.format(url, urlencode(kwargs, doseq=True))
|
||||||
|
return url
|
||||||
|
|
||||||
|
def action(self, name=None):
|
||||||
|
"""
|
||||||
|
Action decorator
|
||||||
|
|
||||||
|
Defines plugin callback action. If action's name is not defined explicitly,
|
||||||
|
then the action is named after the decorated function.
|
||||||
|
|
||||||
|
.. warning:: Action's name must be unique.
|
||||||
|
|
||||||
|
A plugin must have at least one action named ``'root'`` implicitly or explicitly.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@plugin.action() # The action is implicitly named 'root' after the decorated function
|
||||||
|
def root(params):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@plugin.action('foo') # The action name is set explicitly
|
||||||
|
def foo_action(params):
|
||||||
|
pass
|
||||||
|
|
||||||
|
:param name: action's name (optional).
|
||||||
|
:type name: str
|
||||||
|
:raises simpleplugin.SimplePluginError: if the action with such name is already defined.
|
||||||
|
"""
|
||||||
|
def wrap(func, name=name):
|
||||||
|
if name is None:
|
||||||
|
name = func.__name__
|
||||||
|
if name in self.actions:
|
||||||
|
raise SimplePluginError('Action "{0}" already defined!'.format(name))
|
||||||
|
self.actions[name] = func
|
||||||
|
return func
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
def run(self, category=''):
|
||||||
|
"""
|
||||||
|
Run plugin
|
||||||
|
|
||||||
|
:param category: str - plugin sub-category, e.g. 'Comedy'.
|
||||||
|
See :func:`xbmcplugin.setPluginCategory` for more info.
|
||||||
|
:type category: str
|
||||||
|
:raises simpleplugin.SimplePluginError: if unknown action string is provided.
|
||||||
|
"""
|
||||||
|
self._handle = int(sys.argv[1])
|
||||||
|
if category:
|
||||||
|
xbmcplugin.setPluginCategory(self._handle, category)
|
||||||
|
params = self.get_params(sys.argv[2][1:])
|
||||||
|
action = params.get('action', 'root')
|
||||||
|
self.log_debug(str(self))
|
||||||
|
self.log_debug('Actions: {0}'.format(str(self.actions.keys())))
|
||||||
|
self.log_debug('Called action "{0}" with params "{1}"'.format(action, str(params)))
|
||||||
|
try:
|
||||||
|
action_callable = self.actions[action]
|
||||||
|
except KeyError:
|
||||||
|
raise SimplePluginError('Invalid action: "{0}"!'.format(action))
|
||||||
|
else:
|
||||||
|
result = action_callable(params)
|
||||||
|
self.log_debug('Action return value: {0}'.format(str(result)))
|
||||||
|
if isinstance(result, (list, GeneratorType)):
|
||||||
|
self._add_directory_items(self.create_listing(result))
|
||||||
|
elif isinstance(result, basestring):
|
||||||
|
self._set_resolved_url(self.resolve_url(result))
|
||||||
|
elif isinstance(result, tuple) and hasattr(result, 'listing'):
|
||||||
|
self._add_directory_items(result)
|
||||||
|
elif isinstance(result, tuple) and hasattr(result, 'path'):
|
||||||
|
self._set_resolved_url(result)
|
||||||
|
else:
|
||||||
|
self.log_debug('The action "{0}" has not returned any valid data to process.'.format(action))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_listing(listing, succeeded=True, update_listing=False, cache_to_disk=False, sort_methods=None,
|
||||||
|
view_mode=None, content=None):
|
||||||
|
"""
|
||||||
|
Create and return a context dict for a virtual folder listing
|
||||||
|
|
||||||
|
:param listing: the list of the plugin virtual folder items
|
||||||
|
:type listing: :class:`list` or :class:`types.GeneratorType`
|
||||||
|
:param succeeded: if ``False`` Kodi won't open a new listing and stays on the current level.
|
||||||
|
:type succeeded: bool
|
||||||
|
:param update_listing: if ``True``, Kodi won't open a sub-listing but refresh the current one.
|
||||||
|
:type update_listing: bool
|
||||||
|
:param cache_to_disk: cache this view to disk.
|
||||||
|
:type cache_to_disk: bool
|
||||||
|
:param sort_methods: the list of integer constants representing virtual folder sort methods.
|
||||||
|
:type sort_methods: tuple
|
||||||
|
:param view_mode: a numeric code for a skin view mode.
|
||||||
|
View mode codes are different in different skins except for ``50`` (basic listing).
|
||||||
|
:type view_mode: int
|
||||||
|
:param content: string - current plugin content, e.g. 'movies' or 'episodes'.
|
||||||
|
See :func:`xbmcplugin.setContent` for more info.
|
||||||
|
:type content: str
|
||||||
|
:return: context object containing necessary parameters
|
||||||
|
to create virtual folder listing in Kodi UI.
|
||||||
|
:rtype: ListContext
|
||||||
|
"""
|
||||||
|
return ListContext(listing, succeeded, update_listing, cache_to_disk, sort_methods, view_mode, content)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
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 is present, then ``path`` value is 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)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
list_item = xbmcgui.ListItem(label=item.get('label', ''),
|
||||||
|
label2=item.get('label2', ''),
|
||||||
|
path=item.get('path', ''))
|
||||||
|
if int(xbmc.getInfoLabel('System.BuildVersion')[:2]) >= 16:
|
||||||
|
art = item.get('art', {})
|
||||||
|
art['thumb'] = item.get('thumb', '')
|
||||||
|
art['icon'] = item.get('icon', '')
|
||||||
|
art['fanart'] = item.get('fanart', '')
|
||||||
|
item['art'] = art
|
||||||
|
else:
|
||||||
|
list_item.setThumbnailImage(item.get('thumb', ''))
|
||||||
|
list_item.setIconImage(item.get('icon', ''))
|
||||||
|
list_item.setProperty('fanart_image', item.get('fanart', ''))
|
||||||
|
if item.get('art'):
|
||||||
|
list_item.setArt(item['art'])
|
||||||
|
if item.get('stream_info'):
|
||||||
|
for stream, stream_info in item['stream_info'].iteritems():
|
||||||
|
list_item.addStreamInfo(stream, stream_info)
|
||||||
|
if item.get('info'):
|
||||||
|
for media, info in item['info'].iteritems():
|
||||||
|
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'].iteritems():
|
||||||
|
list_item.setProperty(key, value)
|
||||||
|
return list_item
|
||||||
|
|
||||||
|
def _add_directory_items(self, context):
|
||||||
|
"""
|
||||||
|
Create a virtual folder listing
|
||||||
|
|
||||||
|
:param context: context object
|
||||||
|
:type context: ListContext
|
||||||
|
"""
|
||||||
|
self.log_debug('Creating listing from {0}'.format(str(context)))
|
||||||
|
if context.content is not None:
|
||||||
|
xbmcplugin.setContent(self._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 = self.create_list_item(item)
|
||||||
|
if item.get('is_playable'):
|
||||||
|
list_item.setProperty('IsPlayable', 'true')
|
||||||
|
is_folder = False
|
||||||
|
xbmcplugin.addDirectoryItem(self._handle, item['url'], list_item, is_folder)
|
||||||
|
if context.sort_methods is not None:
|
||||||
|
[xbmcplugin.addSortMethod(self._handle, method) for method in context.sort_methods]
|
||||||
|
xbmcplugin.endOfDirectory(self._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 _set_resolved_url(self, context):
|
||||||
|
"""
|
||||||
|
Resolve a playable URL
|
||||||
|
|
||||||
|
:param context: context object
|
||||||
|
:type context: PlayContext
|
||||||
|
"""
|
||||||
|
self.log_debug('Resolving URL from {0}'.format(str(context)))
|
||||||
|
if context.play_item is None:
|
||||||
|
list_item = xbmcgui.ListItem(path=context.path)
|
||||||
|
else:
|
||||||
|
list_item = self.create_list_item(context.play_item)
|
||||||
|
xbmcplugin.setResolvedUrl(self._handle, context.succeeded, list_item)
|
||||||
154
resources/language/English/strings.po
Normal file
154
resources/language/English/strings.po
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
# XBMC Media Center language file
|
||||||
|
# Addon Name: Subsonic
|
||||||
|
# Addon id: plugin.audio.subsonic
|
||||||
|
# Addon Provider:
|
||||||
|
# Addon Translate: Moshkopp
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
msgctxt "#30000"
|
||||||
|
msgid "General"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30001"
|
||||||
|
msgid "Server"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30002"
|
||||||
|
msgid "Server URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30003"
|
||||||
|
msgid "Username"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30004"
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30005"
|
||||||
|
msgid "Display"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30006"
|
||||||
|
msgid "Albums per page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30007"
|
||||||
|
msgid "Tracks per page (ignored in albums & playlists)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30008"
|
||||||
|
msgid "Download"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30009"
|
||||||
|
msgid "Download folder"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30010"
|
||||||
|
msgid "Streaming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30011"
|
||||||
|
msgid "Transcode format"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30012"
|
||||||
|
msgid "Bitrate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30013"
|
||||||
|
msgid "Advanced Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30014"
|
||||||
|
msgid "API version"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30016"
|
||||||
|
msgid "Allow self signed certificates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30017"
|
||||||
|
msgid "Cache (in minutes)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30018"
|
||||||
|
msgid "Cache datas time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30019"
|
||||||
|
msgid "Library"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30020"
|
||||||
|
msgid "Albums"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30021"
|
||||||
|
msgid "Tracks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30022"
|
||||||
|
msgid "Playlists"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30023"
|
||||||
|
msgid "Newest albums"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30024"
|
||||||
|
msgid "Most played albums"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30025"
|
||||||
|
msgid "Recently played albums"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30026"
|
||||||
|
msgid "Random albums"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30029"
|
||||||
|
msgid "Next page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
msgctxt "#30030"
|
||||||
|
msgid "Back to Menu"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30031"
|
||||||
|
msgid "Item has been unstarred."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30032"
|
||||||
|
msgid "Item has been starred!"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30033"
|
||||||
|
msgid "Star on Subsonic"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30034"
|
||||||
|
msgid "Unstar on Subsonic"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30035"
|
||||||
|
msgid "Download"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30036"
|
||||||
|
msgid "Starred tracks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30037"
|
||||||
|
msgid "Random tracks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30038"
|
||||||
|
msgid "Browse"
|
||||||
|
msgstr ""
|
||||||
153
resources/language/French/strings.po
Normal file
153
resources/language/French/strings.po
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# XBMC Media Center language file
|
||||||
|
# Addon Name: Subsonic
|
||||||
|
# Addon id: plugin.audio.subsonic
|
||||||
|
# Addon Provider:
|
||||||
|
# Addon Translate: Gordie
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
msgctxt "#30000"
|
||||||
|
msgid "General"
|
||||||
|
msgstr "Général"
|
||||||
|
|
||||||
|
msgctxt "#30001"
|
||||||
|
msgid "Server"
|
||||||
|
msgstr "Serveur"
|
||||||
|
|
||||||
|
msgctxt "#30002"
|
||||||
|
msgid "Server URL"
|
||||||
|
msgstr "URL du serveur"
|
||||||
|
|
||||||
|
msgctxt "#30003"
|
||||||
|
msgid "Username"
|
||||||
|
msgstr "Nom d'utilisateur"
|
||||||
|
|
||||||
|
msgctxt "#30004"
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Mot de passe"
|
||||||
|
|
||||||
|
msgctxt "#30005"
|
||||||
|
msgid "Display"
|
||||||
|
msgstr "Affichage"
|
||||||
|
|
||||||
|
msgctxt "#30006"
|
||||||
|
msgid "Albums per page"
|
||||||
|
msgstr "Albums par page"
|
||||||
|
|
||||||
|
msgctxt "#30007"
|
||||||
|
msgid "Tracks per page (ignored in albums & playlists)"
|
||||||
|
msgstr "Pistes par page (ignoré dans les albums & listes de lecture)"
|
||||||
|
|
||||||
|
msgctxt "#30008"
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Télécharger"
|
||||||
|
|
||||||
|
msgctxt "#30009"
|
||||||
|
msgid "Download folder"
|
||||||
|
msgstr "Répertoire de téléchargement"
|
||||||
|
|
||||||
|
msgctxt "#30010"
|
||||||
|
msgid "Streaming"
|
||||||
|
msgstr "Diffusion"
|
||||||
|
|
||||||
|
msgctxt "#30011"
|
||||||
|
msgid "Transcode format"
|
||||||
|
msgstr "Format de transcodage"
|
||||||
|
|
||||||
|
msgctxt "#30012"
|
||||||
|
msgid "Bitrate"
|
||||||
|
msgstr "Bitrate"
|
||||||
|
|
||||||
|
msgctxt "#30013"
|
||||||
|
msgid "Advanced Settings"
|
||||||
|
msgstr "Paramètres avancés"
|
||||||
|
|
||||||
|
msgctxt "#30014"
|
||||||
|
msgid "API version"
|
||||||
|
msgstr "Version de l'API"
|
||||||
|
|
||||||
|
msgctxt "#30016"
|
||||||
|
msgid "Allow self signed certificates"
|
||||||
|
msgstr "Autoriser les certificats auto-signés"
|
||||||
|
|
||||||
|
msgctxt "#30017"
|
||||||
|
msgid "Cache (in minutes)"
|
||||||
|
msgstr "Cache (en minutes)"
|
||||||
|
|
||||||
|
msgctxt "#30018"
|
||||||
|
msgid "Cache datas time"
|
||||||
|
msgstr "Durée du cache pour les données"
|
||||||
|
|
||||||
|
msgctxt "#30019"
|
||||||
|
msgid "Library"
|
||||||
|
msgstr "Bibliothèque"
|
||||||
|
|
||||||
|
msgctxt "#30020"
|
||||||
|
msgid "Albums"
|
||||||
|
msgstr "Albums"
|
||||||
|
|
||||||
|
msgctxt "#30021"
|
||||||
|
msgid "Tracks"
|
||||||
|
msgstr "Pistes"
|
||||||
|
|
||||||
|
msgctxt "#30022"
|
||||||
|
msgid "Playlists"
|
||||||
|
msgstr "Playlists"
|
||||||
|
|
||||||
|
msgctxt "#30023"
|
||||||
|
msgid "Newest albums"
|
||||||
|
msgstr "Nouveaux albums"
|
||||||
|
|
||||||
|
msgctxt "#30024"
|
||||||
|
msgid "Most played albums"
|
||||||
|
msgstr "Albums les plus joués"
|
||||||
|
|
||||||
|
msgctxt "#30025"
|
||||||
|
msgid "Recently played albums"
|
||||||
|
msgstr "Albums joués récemment"
|
||||||
|
|
||||||
|
msgctxt "#30026"
|
||||||
|
msgid "Random albums"
|
||||||
|
msgstr "Albums au hasard"
|
||||||
|
|
||||||
|
msgctxt "#30029"
|
||||||
|
msgid "Next page"
|
||||||
|
msgstr "Page suivante"
|
||||||
|
|
||||||
|
msgctxt "#30030"
|
||||||
|
msgid "Back to Menu"
|
||||||
|
msgstr "Retour au menu"
|
||||||
|
|
||||||
|
msgctxt "#30031"
|
||||||
|
msgid "Item has been unstarred."
|
||||||
|
msgstr "Cet élément a été retiré des favoris"
|
||||||
|
|
||||||
|
msgctxt "#30032"
|
||||||
|
msgid "Item has been starred!"
|
||||||
|
msgstr "Cet élément a été ajouté aux favoris !"
|
||||||
|
|
||||||
|
msgctxt "#30033"
|
||||||
|
msgid "Star on Subsonic"
|
||||||
|
msgstr "Ajouter aux favoris Subsonic"
|
||||||
|
|
||||||
|
msgctxt "#30034"
|
||||||
|
msgid "Unstar on Subsonic"
|
||||||
|
msgstr "Retirer des favoris Subsonic"
|
||||||
|
|
||||||
|
msgctxt "#30035"
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Télécharger"
|
||||||
|
|
||||||
|
msgctxt "#30036"
|
||||||
|
msgid "Starred tracks"
|
||||||
|
msgstr "Pistes favorites"
|
||||||
|
|
||||||
|
msgctxt "#30037"
|
||||||
|
msgid "Random tracks"
|
||||||
|
msgstr "Pistes au hasard"
|
||||||
|
|
||||||
|
msgctxt "#30038"
|
||||||
|
msgid "Browse"
|
||||||
|
msgstr "Parcourir"
|
||||||
153
resources/language/German/strings.po
Normal file
153
resources/language/German/strings.po
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# XBMC Media Center language file
|
||||||
|
# Addon Name: Subsonic
|
||||||
|
# Addon id: plugin.audio.subsonic
|
||||||
|
# Addon Provider:
|
||||||
|
# Addon Translate: Moshkopp
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
|
msgctxt "#30000"
|
||||||
|
msgid "General"
|
||||||
|
msgstr "Allgemein"
|
||||||
|
|
||||||
|
msgctxt "#30001"
|
||||||
|
msgid "Server"
|
||||||
|
msgstr "Server"
|
||||||
|
|
||||||
|
msgctxt "#30002"
|
||||||
|
msgid "Server URL"
|
||||||
|
msgstr "Serveradresse"
|
||||||
|
|
||||||
|
msgctxt "#30003"
|
||||||
|
msgid "Username"
|
||||||
|
msgstr "Benutzername"
|
||||||
|
|
||||||
|
msgctxt "#30004"
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Passwort"
|
||||||
|
|
||||||
|
msgctxt "#30005"
|
||||||
|
msgid "Display"
|
||||||
|
msgstr "Anzeige"
|
||||||
|
|
||||||
|
msgctxt "#30006"
|
||||||
|
msgid "Albums per page"
|
||||||
|
msgstr "Alben pro Seite"
|
||||||
|
|
||||||
|
msgctxt "#30007"
|
||||||
|
msgid "Tracks per page (ignored in albums & playlists)"
|
||||||
|
msgstr "Lieder pro Seite (wird in Alben und Playlisten ignoriert)"
|
||||||
|
|
||||||
|
msgctxt "#30008"
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Download"
|
||||||
|
|
||||||
|
msgctxt "#30009"
|
||||||
|
msgid "Download folder"
|
||||||
|
msgstr "Download Verzeichnis"
|
||||||
|
|
||||||
|
msgctxt "#30010"
|
||||||
|
msgid "Streaming"
|
||||||
|
msgstr "Übertragung"
|
||||||
|
|
||||||
|
msgctxt "#30011"
|
||||||
|
msgid "Transcode format"
|
||||||
|
msgstr "Umwandlungs Format"
|
||||||
|
|
||||||
|
msgctxt "#30012"
|
||||||
|
msgid "Bitrate"
|
||||||
|
msgstr "Bitrate"
|
||||||
|
|
||||||
|
msgctxt "#30013"
|
||||||
|
msgid "Advanced Settings"
|
||||||
|
msgstr "Erweitert"
|
||||||
|
|
||||||
|
msgctxt "#30014"
|
||||||
|
msgid "API version"
|
||||||
|
msgstr "API Version"
|
||||||
|
|
||||||
|
msgctxt "#30016"
|
||||||
|
msgid "Allow self signed certificates"
|
||||||
|
msgstr "Erlaube eigensignierte Zertifikate"
|
||||||
|
|
||||||
|
msgctxt "#30017"
|
||||||
|
msgid "Cache (in minutes)"
|
||||||
|
msgstr "Speicher (in Minuten)"
|
||||||
|
|
||||||
|
msgctxt "#30018"
|
||||||
|
msgid "Cache datas time"
|
||||||
|
msgstr "Speicher Daten Zeit"
|
||||||
|
|
||||||
|
msgctxt "#30019"
|
||||||
|
msgid "Library"
|
||||||
|
msgstr "Bibliothek"
|
||||||
|
|
||||||
|
msgctxt "#30020"
|
||||||
|
msgid "Albums"
|
||||||
|
msgstr "Alben"
|
||||||
|
|
||||||
|
msgctxt "#30021"
|
||||||
|
msgid "Tracks"
|
||||||
|
msgstr "Lieder"
|
||||||
|
|
||||||
|
msgctxt "#30022"
|
||||||
|
msgid "Playlists"
|
||||||
|
msgstr "Playlisten"
|
||||||
|
|
||||||
|
msgctxt "#30023"
|
||||||
|
msgid "Newest albums"
|
||||||
|
msgstr "Neueste Alben"
|
||||||
|
|
||||||
|
msgctxt "#30024"
|
||||||
|
msgid "Most played albums"
|
||||||
|
msgstr "Häufig gehörte Alben"
|
||||||
|
|
||||||
|
msgctxt "#30025"
|
||||||
|
msgid "Recently played albums"
|
||||||
|
msgstr "Zuletzt gehörte Alben"
|
||||||
|
|
||||||
|
msgctxt "#30026"
|
||||||
|
msgid "Random albums"
|
||||||
|
msgstr "Zufällige Alben"
|
||||||
|
|
||||||
|
msgctxt "#30029"
|
||||||
|
msgid "Next page"
|
||||||
|
msgstr "Nächste Seite"
|
||||||
|
|
||||||
|
msgctxt "#30030"
|
||||||
|
msgid "Back to Menu"
|
||||||
|
msgstr "Hauptmenü"
|
||||||
|
|
||||||
|
msgctxt "#30031"
|
||||||
|
msgid "Item has been unstarred."
|
||||||
|
msgstr "Bewertung entfernt"
|
||||||
|
|
||||||
|
msgctxt "#30032"
|
||||||
|
msgid "Item has been starred!"
|
||||||
|
msgstr "Bewertung hinzugefügt"
|
||||||
|
|
||||||
|
msgctxt "#30033"
|
||||||
|
msgid "Star on Subsonic"
|
||||||
|
msgstr "Bewerten auf Subsonic"
|
||||||
|
|
||||||
|
msgctxt "#30034"
|
||||||
|
msgid "Unstar on Subsonic"
|
||||||
|
msgstr "Löschen auf Subsonic"
|
||||||
|
|
||||||
|
msgctxt "#30035"
|
||||||
|
msgid "Download"
|
||||||
|
msgstr "Herunterladen"
|
||||||
|
|
||||||
|
msgctxt "#30036"
|
||||||
|
msgid "Starred tracks"
|
||||||
|
msgstr "Lieblings lieder"
|
||||||
|
|
||||||
|
msgctxt "#30037"
|
||||||
|
msgid "Random tracks"
|
||||||
|
msgstr "Zufällig lieder"
|
||||||
|
|
||||||
|
msgctxt "#30038"
|
||||||
|
msgid "Browse"
|
||||||
|
msgstr "Durchsuchen"
|
||||||
@ -1,9 +1,28 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
<settings>
|
<settings>
|
||||||
<setting id="subsonic_url" type="text" label="URL" default="http://demo.subsonic.org"/>
|
<!-- GENERAL -->
|
||||||
<setting id="username" type="text" label="Username" default="guest3"/>
|
<category label="30000">
|
||||||
<setting id="password" type="text" option="hidden" label="Password" default="guest"/>
|
<setting label="30001" type="lsep" />
|
||||||
<setting id="random_count" type="labelenum" label="Random songs" values="10|15|20|25"/>
|
<setting label="30002" id="subsonic_url" type="text" default="http://demo.subsonic.org"/>
|
||||||
<setting id="transcode_format" type="labelenum" label="Transcode format" values="mp3|raw|flv|ogg"/>
|
<setting label="30003" id="username" type="text" default="guest3"/>
|
||||||
<setting id="bitrate" type="labelenum" label="Bitrate" values="320|256|224|192|160|128|112|96|80|64|56|48|40|32"/>
|
<setting label="30004" id="password" type="text" option="hidden" default="guest"/>
|
||||||
|
<setting label="30005" type="lsep" />
|
||||||
|
<setting label="30006" id="albums_per_page" type="labelenum" default="50" values="10|25|50|100|250|500"/>
|
||||||
|
<setting label="30007" id="tracks_per_page" type="labelenum" default="100" values="10|25|50|100|250|500"/>
|
||||||
|
<setting label="30008" type="lsep" />
|
||||||
|
<setting label="30009" id="download_folder" type="folder" source="auto" option="writeable"/>
|
||||||
|
<setting label="30010" type="lsep" />
|
||||||
|
<setting label="30011" id="transcode_format_streaming" type="labelenum" values="mp3|raw|flv|ogg"/>
|
||||||
|
<setting label="30012" id="bitrate_streaming" type="labelenum" values="320|256|224|192|160|128|112|96|80|64|56|48|40|32"/>
|
||||||
|
</category>
|
||||||
|
|
||||||
|
<!-- ADVANCED -->
|
||||||
|
<category label="30013">
|
||||||
|
<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" default="1.13.0"/>
|
||||||
|
<setting label="30016" id="insecure" type="bool" default="false" />
|
||||||
|
<setting label="30017" type="lsep" />
|
||||||
|
<setting label="30018" id="cachetime" type="labelenum" default="15" values="1|5|15|30|60|120|180|720|1440"/>
|
||||||
|
|
||||||
|
</category>
|
||||||
</settings>
|
</settings>
|
||||||
|
|||||||
Reference in New Issue
Block a user