86 Commits

Author SHA1 Message Date
358df44fb1 Cleanup and prepare for merge 2021-09-02 16:54:48 +10:00
a551df5c8c Clean up deprecation warnings and add some logging to resolve http client timeout 2021-09-02 12:56:34 +10:00
e640d0f81d Update Readme to include Navidrome compatibility 2021-08-31 09:22:18 +10:00
3aac9d1c54 Change to use GET as default 2021-08-31 09:16:03 +10:00
7564e75f63 Catch empty star response 2021-08-31 08:52:05 +10:00
ed3f7da8b5 Remove dependency for integer ids to suit Navidrome requirement 2021-08-30 16:42:28 +10:00
194844571c Fix ndent 2021-07-02 18:12:01 +10:00
6035ff49b8 Avoid errors in walk functions 2021-07-02 17:37:44 +10:00
bc30d36466 Correct fanart path in addon.xml 2021-07-02 08:57:04 +10:00
445303a2fc Merge pull request #30 from warwickh/master
Merge Matrix compatible to master
2021-06-30 11:13:38 +02:00
2d76c74f42 All logging to LOGDEBUG 2021-06-29 20:28:48 +10:00
a7f74582de Add fanart 2021-06-29 20:05:09 +10:00
41a747a2b7 Add fanart 2021-06-29 20:02:35 +10:00
2e5d012cae Add fanart 2021-06-29 20:01:30 +10:00
53ecd6e5d1 Add fanart 2021-06-29 19:56:16 +10:00
2efa209227 Clean up main.py 2021-06-29 15:47:38 +10:00
1703d433a3 Move Matrix compatible version to 3.0.0 2021-06-29 14:47:07 +10:00
17f77aca93 Change layout 2021-06-29 14:42:03 +10:00
a136a7b47e Change layout 2021-06-29 14:40:26 +10:00
b461af0269 Change layout 2021-06-29 14:39:39 +10:00
bf1ee6bd1b Update installation instructions 2021-06-29 14:39:16 +10:00
79a9b1ebc3 Updated simpleplugin link 2021-06-29 14:32:53 +10:00
46da611895 Update contact information 2021-06-29 14:14:58 +10:00
d524b8cc8e set version for script.module.future 2021-06-29 14:14:07 +10:00
0ddda4676c Added requirement for script.module.future 2021-06-29 07:38:03 +10:00
1d22cd75c1 Update settings.xml 2021-06-28 19:26:44 +10:00
1fc06bd790 Add api version 1.16.0 2021-06-28 19:21:36 +10:00
e958c5bb2a Stream and coverart requests assume url request and add useget by default in connection 2021-06-28 19:07:56 +10:00
9d86ff2051 Readd some logging for stream auth 2021-06-28 17:59:22 +10:00
71e208b662 Need to check why useget required for streaming 2021-06-28 17:51:27 +10:00
6910755b36 Update settings.xml
Ensure default bitrate
2021-06-28 17:41:01 +10:00
aca6ad6e83 Change port storage to text 2021-06-28 16:50:21 +10:00
adc78f9783 Clean up some logging 2021-06-28 15:47:08 +10:00
0c32547c0d Update readme 2021-06-28 15:40:33 +10:00
67b5e4ab8c Update readme 2021-06-28 15:13:17 +10:00
861e7534c6 Fix connection port issue 2021-06-28 14:55:56 +10:00
c65548a73b Fix empty folder crash 2021-06-28 14:06:59 +10:00
f5202f2229 Restore notes against TO FIX items 2021-06-23 09:56:28 +10:00
2b3789fe5c Fix browse function 2021-06-22 18:25:52 +10:00
92356ab533 2.0.9 2021-06-22 16:47:09 +10:00
1d3181fe4e Initial Matrix Compatible commit 2021-06-22 16:35:39 +10:00
577181bb73 Merge pull request #20 from gordielachance/2.0.8
2 0 8
2017-12-04 11:35:04 +01:00
f0649bf2b5 Fix missing DE translation for "Search" 2017-12-03 18:32:32 +01:00
b630801150 Merge pull request #19 from comxd/patch-1
Fix missing FR translation for "Search"
2017-12-03 18:30:06 +01:00
99d57fa1f9 Fix missing FR translation for "Search"
@see commit #9e7e542585eb67392fad3191f44a973c0e59839f
2017-12-03 18:16:21 +01:00
f7ccef89ef 2.0.8 2017-11-29 11:30:58 +01:00
d90af02d8b Merge pull request #18 from Heruwar/feature/path-support_and_pw-improvements
Path support and security improvements
2017-11-29 11:09:56 +01:00
4429451454 Dont sent plaintext password in requests and use subsonic hex-encoding with legacy auth 2017-11-25 18:15:12 +01:00
313b413fc5 Add support for path in server URL 2017-11-25 18:13:44 +01:00
30d0e8641e Make sure password and username are always strings 2017-11-25 18:12:53 +01:00
b18b8e0094 Update addon.xml 2017-04-18 15:26:34 +02:00
66ae5ee093 Update CHANGELOG.md 2017-04-18 15:25:20 +02:00
426dcf964f Merge pull request #16 from silascutler/master
Added Search
2017-04-18 15:19:10 +02:00
ce58b6993f Updates 2017-04-02 12:12:49 -04:00
f51e5c3c28 Removed pop-up option 2017-04-02 11:44:22 -04:00
e966b5bfa2 Removed pop-up option 2017-04-02 11:44:14 -04:00
9e7e542585 Added Search 2017-04-02 09:06:49 -04:00
10bb432a14 Merge pull request #12 from gordielachance/media-folders
Media folders
2017-01-14 12:47:36 +01:00
e6b00afc27 version update 2017-01-14 12:38:50 +01:00
60ebe928b0 minor 2017-01-10 18:36:34 +01:00
2482e56fdc minor 2017-01-09 11:36:23 +01:00
290fa837b3 simpleplugin 2.1.0 2017-01-09 11:01:41 +01:00
0344163cd5 typo 2017-01-09 10:50:00 +01:00
3a0efadebb use Addon().get_localized_string (simpleplugin) 2017-01-09 10:48:30 +01:00
8c2ffe006c localized strings : settings typo 2017-01-09 10:46:24 +01:00
ebf82b8218 fixes for localized strings 2017-01-09 10:46:11 +01:00
d9a18d41e3 reorganize code 2017-01-09 10:45:12 +01:00
c6e9cc92e8 Browse/Library menus (file structure vs ID3 tags) 2017-01-09 01:02:37 +01:00
3ed7b231cc if there is only one media folder, return list_indexes() directly 2017-01-08 11:35:53 +01:00
7d26333ce0 WIP : list_indexes function (not working yet) 2017-01-08 11:11:05 +01:00
d9210dcaef new 'Browse' top menu to list media folders 2017-01-08 11:09:54 +01:00
0cda25e07a Merge pull request #9 from Moshkopp/patch-2
german description.
2017-01-03 23:19:38 +01:00
be07844e38 german description. 2017-01-03 22:56:07 +01:00
5484614cc1 typo 2017-01-03 22:07:47 +01:00
7e67edd64a removed some strings
+ typos
+ changelog
2017-01-03 21:14:00 +01:00
8ce871a9f7 typo 2017-01-03 20:54:51 +01:00
3acb61d605 french translation 2017-01-03 20:40:08 +01:00
c621458704 Merge pull request #7 from Moshkopp/master
Added multilanguage support
2017-01-03 20:14:41 +01:00
aaac00f69a Update strings.po 2016-12-28 18:13:22 +01:00
afea46a9e1 Add files via upload
typo
2016-12-28 18:12:29 +01:00
fb64a2e6fe Delete strings.po 2016-12-28 18:12:03 +01:00
a029490fe8 Add files via upload
typo
2016-12-28 18:10:05 +01:00
4a057ba83c Add files via upload
Added multilanguage support
2016-12-28 14:33:26 +01:00
ef6ba710e2 doc 2016-10-15 01:33:55 +02:00
4179ca8ada Fixed images when listing entries 2016-10-15 01:33:43 +02:00
fcbcccaeb9 doc 2016-10-15 01:27:12 +02:00
14 changed files with 2689 additions and 1496 deletions

View File

@ -1,5 +1,35 @@
# Changelog
## v3.0.0
Released 29th June 2021 (by warwickh)
* Basic update to provide Matrix compatility. Not tested on Kodi below v19
* Updates simpleplugin to latest version (3.0.0) https://github.com/vlmaksime/script.module.simpleplugin
* Moves some legacy simpleplugin static routines into main.py
* Removes dependancy on libsonic_extra by moving some walk functions into main.py
* Updates libsonic to latest version and adds functions for returning raw url for populating menus
* Move to version 3+ for diffferentiation from Leia compatible version
## v2.0.8
Released 29th November 2017 (by Heruwar)
* Fixes a security issue where the password is sent as plaintext in the URL query parameters when methods from libsonic_extas are used.
Also adds Subsonic hex encoding when using legacy auth.
* Adds support for URL paths like https://hostname.com/subsonic as requested in Github issue #17 and also encountered in some of the reports (#14 and #5)
* Fixes an error when the password only contains digits, which simpleplugin converts to a Long, which later fails when libsonic tries to salt the password expecting a string.
## v2.0.7
Released 18 April 2017
* Added Search (by silascutler)
## 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

View File

@ -1,20 +1,34 @@
# Subsonic
Kodi plugin to stream, star and download music from Subsonic.
Kodi plugin to stream, star and download music from Subsonic/Airsonic/Navidrome (requires Subsonic API compatibility)
For feature requests / issues:
https://github.com/gordielachance/plugin.audio.subsonic/issues
https://github.com/warwickh/plugin.audio.subsonic/issues
Contributions are welcome:
https://github.com/gordielachance/plugin.audio.subsonic
https://github.com/warwickh/plugin.audio.subsonic
Master branch updated to support Kodi 19 Matrix
Leia compatible version available in alternate branch
## Features
* Browse by artist, albums (newest/most played/recently played/random), tracks (starred/random), and playlists
* Download songs
* Star songs
* Navidrome compatibility added (please report any issues)
## Installation
* Click the code button and download
* Enable unknown sources and install from zip in Kodi
or
* Navigate to your `.kodi/addons/` folder
* Clone this repository: `git clone https://github.com/gordielachance/plugin.audio.subsonic.git`
* Clone this repository: `git clone https://github.com/warwickh/plugin.audio.subsonic.git`
* (Re)start Kodi.
Note: You may need to install dependencies manually if installing this way. I recommend installing from zip first, then updating using git clone
## TODO
* Scrobble to Last.FM (http://forum.kodi.tv/showthread.php?tid=195597&pid=2429362#pid2429362)
* Improve the caching system
@ -24,7 +38,8 @@ https://github.com/gordielachance/plugin.audio.subsonic
See the `LICENSE` file.
Additional copyright notices:
* [Previous version of this plugin](https://github.com/gordielachance/plugin.audio.subsonic) by gordielachance
* [Previous version of this plugin](https://github.com/basilfx/plugin.audio.subsonic) by basilfx
* [SimplePlugin](https://github.com/romanvm/script.module.simpleplugin/stargazers) by romanvm
* [SimplePlugin](https://github.com/romanvm/script.module.simpleplugin/stargazers) by romanvm now at [SimplePlugin3](https://github.com/vlmaksime/script.module.simpleplugin)
* The original [SubKodi](https://github.com/DarkAllMan/SubKodi) plugin
* [`py-sonic`](https://github.com/crustymonkey/py-sonic) Python module

View File

@ -1,28 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.audio.subsonic" name="Subsonic" version="2.0.4" provider-name="BasilFX,grosbouff,lrusak">
<addon id="plugin.audio.subsonic" name="Subsonic" version="3.0.0" provider-name="BasilFX,grosbouff,silascutler,Heruwar,warwickh">
<requires>
<import addon="xbmc.python" version="2.14.0"/>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.dateutil" version="2.4.2"/>
<import addon="script.module.future" version="0.18.2"/>
</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>
<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
https://github.com/warwickh/plugin.audio.subsonic/issues
Contributions are welcome:
https://github.com/gordielachance/plugin.audio.subsonic
https://github.com/warwickh/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/warwickh/plugin.audio.subsonic/issues
Les contributions sont les bienvenues :
https://github.com/warwickh/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/warwickh/plugin.audio.subsonic/issues
Beihilfe ist Willkommen:
https://github.com/warwickh/plugin.audio.subsonic
</description>
<assets>
<icon>icon.png</icon>
<fanart>fanart.jpg</fanart>
</assets>
<disclaimer lang="en"></disclaimer>
<language>en</language>
<language>multi</language>
<platform>all</platform>
<license>MIT</license>
<forum></forum>
<website>http://www.subsonic.org</website>
<source>https://github.com/gordielachance/plugin.audio.subsonic</source>
<source>https://github.com/warwickh/plugin.audio.subsonic</source>
<email></email>
</extension>
</addon>

BIN
fanart.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -1,32 +1,24 @@
"""
This file is part of py-sonic.
py-sonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
py-sonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with py-sonic. If not, see <http://www.gnu.org/licenses/>
For information on method calls, see 'pydoc libsonic.connection'
----------
Basic example:
----------
import libsonic
conn = libsonic.Connection('http://localhost' , 'admin' , 'password')
print conn.ping()
"""
from connection import *
from .connection import *
__version__ = '0.6.2'
__version__ = '0.7.9'

View File

@ -15,23 +15,28 @@ You should have received a copy of the GNU General Public License
along with py-sonic. If not, see <http://www.gnu.org/licenses/>
"""
from urllib import urlencode
from .errors import *
from pprint import pprint
from cStringIO import StringIO
from libsonic.errors import *
from netrc import netrc
from hashlib import md5
import json, urllib2, httplib, logging, socket, ssl, sys, os
import urllib.request
import urllib.error
from http import client as http_client
from urllib.parse import urlencode
from io import StringIO
API_VERSION = '1.14.0'
import json
import logging
import socket
import ssl
import sys
import os
import xbmc
API_VERSION = '1.16.1'
logger = logging.getLogger(__name__)
class HTTPSConnectionChain(httplib.HTTPSConnection):
_preferred_ssl_protos = sorted([ p for p in dir(ssl)
if p.startswith('PROTOCOL_') ], reverse=True)
_ssl_working_proto = None
class HTTPSConnectionChain(http_client.HTTPSConnection):
def _create_sock(self):
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
@ -40,38 +45,21 @@ class HTTPSConnectionChain(httplib.HTTPSConnection):
return sock
def connect(self):
if self._ssl_working_proto is not None:
# If we have a working proto, let's use that straight away
logger.debug("Using known working proto: '%s'",
self._ssl_working_proto)
sock = self._create_sock()
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
ssl_version=self._ssl_working_proto)
return
# Try connecting via the different SSL protos in preference order
for proto_name in self._preferred_ssl_protos:
sock = self._create_sock()
proto = getattr(ssl, proto_name, None)
try:
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
ssl_version=proto)
self.sock = ssl.create_default_context().wrap_socket(sock,
server_hostname=self.host)
except:
sock.close()
else:
# Cache the working ssl version
HTTPSConnectionChain._ssl_working_proto = proto
break
class HTTPSHandlerChain(urllib2.HTTPSHandler):
class HTTPSHandlerChain(urllib.request.HTTPSHandler):
def https_open(self, req):
return self.do_open(HTTPSConnectionChain, req)
# install opener
urllib2.install_opener(urllib2.build_opener(HTTPSHandlerChain()))
urllib.request.install_opener(urllib.request.build_opener(HTTPSHandlerChain()))
class PysHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
class PysHTTPRedirectHandler(urllib.request.HTTPRedirectHandler):
"""
This class is used to override the default behavior of the
HTTPRedirectHandler, which does *not* redirect POST data
@ -81,24 +69,30 @@ class PysHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
if (code in (301, 302, 303, 307) and m in ("GET", "HEAD")
or code in (301, 302, 303) and m == "POST"):
newurl = newurl.replace(' ', '%20')
newheaders = dict((k, v) for k, v in req.headers.items()
newheaders = dict((k, v) for k, v in list(req.headers.items())
if k.lower() not in ("content-length", "content-type")
)
data = None
if req.has_data():
data = req.get_data()
return urllib2.Request(newurl,
if req.data:
data = req.data
return urllib.request.Request(newurl,
data=data,
headers=newheaders,
origin_req_host=req.get_origin_req_host(),
origin_req_host=req.origin_req_host,
unverifiable=True)
else:
raise urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp)
raise urllib.error.HTTPError(
req.get_full_url(),
code,
msg,
headers,
fp,
)
class Connection(object):
def __init__(self, baseUrl, username=None, password=None, port=4040,
serverPath='/rest', appName='py-sonic', apiVersion=API_VERSION,
insecure=False, useNetrc=None, legacyAuth=False, useGET=False):
insecure=False, useNetrc=None, legacyAuth=False, useGET=True):
"""
This will create a connection to your subsonic server
@ -271,6 +265,52 @@ class Connection(object):
self._checkStatus(res)
return res
def getScanStatus(self):
"""
since: 1.15.0
returns the current status for media library scanning.
takes no extra parameters.
returns a dict like the following:
{'status': 'ok', 'version': '1.15.0',
'scanstatus': {'scanning': true, 'count': 4680}}
'count' is the total number of items to be scanned
"""
methodName = 'getScanStatus'
viewName = '%s.view' % methodName
req = self._getRequest(viewName)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
def startScan(self):
"""
since: 1.15.0
Initiates a rescan of the media libraries.
Takes no extra parameters.
returns a dict like the following:
{'status': 'ok', 'version': '1.15.0',
'scanstatus': {'scanning': true, 'count': 0}}
'scanning' changes to false when a scan is complete
'count' starts a 0 and ends at the total number of items scanned
"""
methodName = 'startScan'
viewName = '%s.view' % methodName
req = self._getRequest(viewName)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
def getMusicFolders(self):
"""
since: 1.0.0
@ -809,6 +849,58 @@ class Connection(object):
self._checkStatus(res)
return res
def streamUrl(self, sid, maxBitRate=0, tformat=None, timeOffset=None,
size=None, estimateContentLength=False, converted=False):
"""
since: 1.0.0
Downloads a given music file.
sid:str The ID of the music file to download.
maxBitRate:int (since: 1.2.0) If specified, the server will
attempt to limit the bitrate to this value, in
kilobits per second. If set to zero (default), no limit
is imposed. Legal values are: 0, 32, 40, 48, 56, 64,
80, 96, 112, 128, 160, 192, 224, 256 and 320.
tformat:str (since: 1.6.0) Specifies the target format
(e.g. "mp3" or "flv") in case there are multiple
applicable transcodings (since: 1.9.0) You can use
the special value "raw" to disable transcoding
timeOffset:int (since: 1.6.0) Only applicable to video
streaming. Start the stream at the given
offset (in seconds) into the video
size:str (since: 1.6.0) The requested video size in
WxH, for instance 640x480
estimateContentLength:bool (since: 1.8.0) If set to True,
the HTTP Content-Length header
will be set to an estimated
value for trancoded media
converted:bool (since: 1.14.0) Only applicable to video streaming.
Subsonic can optimize videos for streaming by
converting them to MP4. If a conversion exists for
the video in question, then setting this parameter
to "true" will cause the converted video to be
returned instead of the original.
Returns the file-like object for reading or raises an exception
on error
"""
methodName = 'stream'
viewName = '%s.view' % methodName
q = self._getQueryDict({'id': sid, 'maxBitRate': maxBitRate,
'format': tformat, 'timeOffset': timeOffset, 'size': size,
'estimateContentLength': estimateContentLength,
'converted': converted})
req = self._getRequest(viewName, q)
xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
#res = self._doBinReq(req)
#if isinstance(res, dict):
# self._checkStatus(res)
return req.full_url
def getCoverArt(self, aid, size=None):
"""
since: 1.0.0
@ -832,6 +924,31 @@ class Connection(object):
self._checkStatus(res)
return res
def getCoverArtUrl(self, aid, size=None):
"""
since: 1.0.0
Returns a cover art image
aid:str ID string for the cover art image to download
size:int If specified, scale image to this size
Returns the file-like object for reading or raises an exception
on error
"""
methodName = 'getCoverArt'
viewName = '%s.view' % methodName
q = self._getQueryDict({'id': aid, 'size': size})
req = self._getRequest(viewName, q)
xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
#res = self._doBinReq(req)
#if isinstance(res, dict):
# self._checkStatus(res)
return req.full_url
def scrobble(self, sid, submission=True, listenTime=None):
"""
since: 1.5.0
@ -980,7 +1097,7 @@ class Connection(object):
streamRole=True, jukeboxRole=False, downloadRole=False,
uploadRole=False, playlistRole=False, coverArtRole=False,
commentRole=False, podcastRole=False, shareRole=False,
musicFolderId=None):
videoConversionRole=False, musicFolderId=None):
"""
since: 1.1.0
@ -1011,6 +1128,7 @@ class Connection(object):
'uploadRole': uploadRole, 'playlistRole': playlistRole,
'coverArtRole': coverArtRole, 'commentRole': commentRole,
'podcastRole': podcastRole, 'shareRole': shareRole,
'videoConversionRole': videoConversionRole,
'musicFolderId': musicFolderId
})
@ -1024,7 +1142,7 @@ class Connection(object):
streamRole=True, jukeboxRole=False, downloadRole=False,
uploadRole=False, playlistRole=False, coverArtRole=False,
commentRole=False, podcastRole=False, shareRole=False,
musicFolderId=None, maxBitRate=0):
videoConversionRole=False, musicFolderId=None, maxBitRate=0):
"""
since 1.10.1
@ -1056,6 +1174,7 @@ class Connection(object):
'uploadRole': uploadRole, 'playlistRole': playlistRole,
'coverArtRole': coverArtRole, 'commentRole': commentRole,
'podcastRole': podcastRole, 'shareRole': shareRole,
'videoConversionRole': videoConversionRole,
'musicFolderId': musicFolderId, 'maxBitRate': maxBitRate
})
req = self._getRequest(viewName, q)
@ -1874,6 +1993,7 @@ class Connection(object):
q['musicFolderId'] = musicFolderId
req = self._getRequest(viewName, q)
xbmc.log("Requesting %s"%str(req.full_url),xbmc.LOGDEBUG)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
@ -1960,7 +2080,7 @@ class Connection(object):
req = self._getRequest(viewName, q)
try:
res = self._doBinReq(req)
except urllib2.HTTPError:
except urllib.error.HTTPError:
# Avatar is not set/does not exist, return None
return None
if isinstance(res, dict):
@ -2065,7 +2185,7 @@ class Connection(object):
musicFolderId:int Only return results from the music folder
with the given ID. See getMusicFolders
"""
methodName = 'getGenres'
methodName = 'getSongsByGenre'
viewName = '%s.view' % methodName
q = self._getQueryDict({'genre': genre,
@ -2112,7 +2232,7 @@ class Connection(object):
req = self._getRequest(viewName, q)
try:
res = self._doBinReq(req)
except urllib2.HTTPError:
except urllib.error.HTTPError:
# Avatar is not set/does not exist, return None
return None
if isinstance(res, dict):
@ -2224,6 +2344,70 @@ class Connection(object):
self._checkStatus(res)
return res
def createInternetRadioStation(self, streamUrl, name, homepageUrl=None):
"""
since 1.16.0
Create an internet radio station
streamUrl:str The stream URL for the station
name:str The user-defined name for the station
homepageUrl:str The homepage URL for the station
"""
methodName = 'createInternetRadioStation'
viewName = '{}.view'.format(methodName)
q = self._getQueryDict({
'streamUrl': streamUrl, 'name': name, 'homepageUrl': homepageUrl})
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
def updateInternetRadioStation(self, iid, streamUrl, name,
homepageUrl=None):
"""
since 1.16.0
Create an internet radio station
iid:str The ID for the station
streamUrl:str The stream URL for the station
name:str The user-defined name for the station
homepageUrl:str The homepage URL for the station
"""
methodName = 'updateInternetRadioStation'
viewName = '{}.view'.format(methodName)
q = self._getQueryDict({
'id': iid, 'streamUrl': streamUrl, 'name': name,
'homepageUrl': homepageUrl,
})
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
def deleteInternetRadioStation(self, iid):
"""
since 1.16.0
Create an internet radio station
iid:str The ID for the station
"""
methodName = 'deleteInternetRadioStation'
viewName = '{}.view'.format(methodName)
q = {'id': iid}
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
return res
def getBookmarks(self):
"""
since: 1.9.0
@ -2490,7 +2674,8 @@ class Connection(object):
methodName = 'getVideoInfo'
viewName = '%s.view' % methodName
q = {'id': int(vid)}
#q = {'id': int(vid)}
q = {'id': vid}
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
@ -2507,7 +2692,8 @@ class Connection(object):
methodName = 'getAlbumInfo'
viewName = '%s.view' % methodName
q = {'id': int(aid)}
#q = {'id': int(aid)}
q = {'id': aid}
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
@ -2524,7 +2710,8 @@ class Connection(object):
methodName = 'getAlbumInfo2'
viewName = '%s.view' % methodName
q = {'id': int(aid)}
#q = {'id': int(aid)}
q = {'id': aid}
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
@ -2543,7 +2730,8 @@ class Connection(object):
methodName = 'getCaptions'
viewName = '%s.view' % methodName
q = self._getQueryDict({'id': int(vid), 'format': fmt})
#q = self._getQueryDict({'id': int(vid), 'format': fmt})
q = self._getQueryDict({'id': vid, 'format': fmt})
req = self._getRequest(viewName, q)
res = self._doInfoReq(req)
self._checkStatus(res)
@ -2561,7 +2749,7 @@ class Connection(object):
url = '%s:%d/%s/%s?%s' % (self._baseUrl, self._port,
self._separateServerPath(), viewName, methodName)
req = urllib2.Request(url)
req = urllib.request.Request(url)
res = self._opener.open(req)
res_msg = res.msg.lower()
return res_msg == 'ok'
@ -2575,14 +2763,17 @@ class Connection(object):
if sys.version_info[:3] >= (2, 7, 9) and self._insecure:
https_chain = HTTPSHandlerChain(
context=ssl._create_unverified_context())
opener = urllib2.build_opener(PysHTTPRedirectHandler, https_chain)
opener = urllib.request.build_opener(
PysHTTPRedirectHandler,
https_chain,
)
return opener
def _getQueryDict(self, d):
"""
Given a dictionary, it cleans out all the values set to None
"""
for k, v in d.items():
for k, v in list(d.items()):
if v is None:
del d[k]
return d
@ -2599,7 +2790,7 @@ class Connection(object):
qdict['p'] = 'enc:%s' % self._hexEnc(self._rawPass)
else:
salt = self._getSalt()
token = md5(self._rawPass + salt).hexdigest()
token = md5((self._rawPass + salt).encode('utf-8')).hexdigest()
qdict.update({
's': salt,
't': token,
@ -2612,12 +2803,13 @@ class Connection(object):
qdict.update(query)
url = '%s:%d/%s/%s' % (self._baseUrl, self._port, self._serverPath,
viewName)
req = urllib2.Request(url, urlencode(qdict))
if self._useGET:
#xbmc.log("Standard URL %s"%url,level=xbmc.LOGDEBUG)
#xbmc.log("Qdict %s"%str(qdict),level=xbmc.LOGDEBUG)
req = urllib.request.Request(url, urlencode(qdict).encode('utf-8'))
if(self._useGET or ('getCoverArt' in viewName) or ('stream' in viewName)):
url += '?%s' % urlencode(qdict)
req = urllib2.Request(url)
#xbmc.log("UseGET URL %s"%(url),xbmc.LOGDEBUG)
req = urllib.request.Request(url)
return req
def _getRequestWithList(self, viewName, listName, alist, query={}):
@ -2633,7 +2825,7 @@ class Connection(object):
data.write(urlencode(qdict))
for i in alist:
data.write('&%s' % urlencode({listName: i}))
req = urllib2.Request(url, data.getvalue())
req = urllib.request.Request(url, data.getvalue().encode('utf-8'))
if self._useGET:
url += '?%s' % data.getvalue()
@ -2657,10 +2849,10 @@ class Connection(object):
viewName)
data = StringIO()
data.write(urlencode(qdict))
for k, l in listMap.iteritems():
for k, l in listMap.items():
for i in l:
data.write('&%s' % urlencode({k: i}))
req = urllib2.Request(url, data.getvalue())
req = urllib.request.Request(url, data.getvalue().encode('utf-8'))
if self._useGET:
url += '?%s' % data.getvalue()
@ -2671,12 +2863,17 @@ class Connection(object):
def _doInfoReq(self, req):
# Returns a parsed dictionary version of the result
res = self._opener.open(req)
dres = json.loads(res.read())
dres = json.loads(res.read().decode('utf-8'))
return dres['subsonic-response']
def _doBinReq(self, req):
res = self._opener.open(req)
contType = res.info().getheader('Content-Type')
info = res.info()
if hasattr(info, 'getheader'):
contType = info.getheader('Content-Type')
else:
contType = info.get('Content-Type')
if contType:
if contType.startswith('text/html') or \
contType.startswith('application/json'):
@ -2716,7 +2913,7 @@ class Connection(object):
"""
separate REST portion of URL from base server path.
"""
return urllib2.splithost(self._serverPath)[1].split('/')[0]
return urllib.parse.splithost(self._serverPath)[1].split('/')[0]
def _fixLastModified(self, data):
"""
@ -2726,9 +2923,9 @@ class Connection(object):
of SECONDS since the unix epoch. JAVA SUCKS!
"""
if isinstance(data, dict):
for k, v in data.items():
for k, v in list(data.items()):
if k == 'lastModified':
data[k] = long(v) / 1000.0
data[k] = int(v) / 1000.0
return
elif isinstance(v, (tuple, list, dict)):
return self._fixLastModified(v)

View File

@ -1,434 +0,0 @@
import urllib
import urlparse
import libsonic
def force_list(value):
"""
Coerce the input value to a list.
If `value` is `None`, return an empty list. If it is a single value, create
a new list with that element on index 0.
:param value: Input value to coerce.
:return: Value as list.
:rtype: list
"""
if value is None:
return []
elif type(value) == list:
return value
else:
return [value]
class SubsonicClient(libsonic.Connection):
"""
Extend `libsonic.Connection` with new features and fix a few issues.
- Parse URL for host and port for constructor.
- Make sure API results are of of uniform type.
- Provide methods to intercept URL of binary requests.
- Add order property to playlist items.
- Add conventient `walk_*' methods to iterate over the API responses.
"""
def __init__(self, url, username, password, apiversion, insecure, legacyauth):
"""
Construct a new SubsonicClient.
:param str url: Full URL (including scheme) of the Subsonic server.
:param str username: Username of the server.
:param str password: Password of the server.
"""
self.intercept_url = False
# Parse Subsonic URL
parts = urlparse.urlparse(url)
scheme = parts.scheme or "http"
# Make sure there is hostname
if not parts.hostname:
raise ValueError("Expected hostname for URL: %s" % url)
# Validate scheme
if scheme not in ("http", "https"):
raise ValueError("Unexpected scheme '%s' for URL: %s" % (
scheme, url))
# Pick a default port
host = "%s://%s" % (scheme, parts.hostname)
port = parts.port or {"http": 80, "https": 443}[scheme]
# Invoke original constructor
super(SubsonicClient, self).__init__(
host, username, password, port=port, appName='Kodi', apiVersion=apiversion, insecure=insecure, legacyAuth=legacyauth)
def getIndexes(self, *args, **kwargs):
"""
Improve the getIndexes method. Ensures IDs are integers.
"""
def _artists_iterator(artists):
for artist in force_list(artists):
artist["id"] = int(artist["id"])
yield artist
def _index_iterator(index):
for index in force_list(index):
index["artist"] = list(_artists_iterator(index.get("artist")))
yield index
def _children_iterator(children):
for child in force_list(children):
child["id"] = int(child["id"])
if "parent" in child:
child["parent"] = int(child["parent"])
if "coverArt" in child:
child["coverArt"] = int(child["coverArt"])
if "artistId" in child:
child["artistId"] = int(child["artistId"])
if "albumId" in child:
child["albumId"] = int(child["albumId"])
yield child
response = super(SubsonicClient, self).getIndexes(*args, **kwargs)
response["indexes"] = response.get("indexes", {})
response["indexes"]["index"] = list(
_index_iterator(response["indexes"].get("index")))
response["indexes"]["child"] = list(
_children_iterator(response["indexes"].get("child")))
return response
def getPlaylists(self, *args, **kwargs):
"""
Improve the getPlaylists method. Ensures IDs are integers.
"""
def _playlists_iterator(playlists):
for playlist in force_list(playlists):
playlist["id"] = int(playlist["id"])
yield playlist
response = super(SubsonicClient, self).getPlaylists(*args, **kwargs)
response["playlists"]["playlist"] = list(
_playlists_iterator(response["playlists"].get("playlist")))
return response
def getPlaylist(self, *args, **kwargs):
"""
Improve the getPlaylist method. Ensures IDs are integers and add an
order property to each entry.
"""
def _entries_iterator(entries):
for order, entry in enumerate(force_list(entries), start=1):
entry["id"] = int(entry["id"])
entry["order"] = order
yield entry
response = super(SubsonicClient, self).getPlaylist(*args, **kwargs)
response["playlist"]["entry"] = list(
_entries_iterator(response["playlist"].get("entry")))
return response
def getArtists(self, *args, **kwargs):
"""
Improve the getArtists method. Ensures IDs are integers.
"""
def _artists_iterator(artists):
for artist in force_list(artists):
artist["id"] = int(artist["id"])
yield artist
def _index_iterator(index):
for index in force_list(index):
index["artist"] = list(_artists_iterator(index.get("artist")))
yield index
response = super(SubsonicClient, self).getArtists(*args, **kwargs)
response["artists"] = response.get("artists", {})
response["artists"]["index"] = list(
_index_iterator(response["artists"].get("index")))
return response
def getArtist(self, *args, **kwargs):
"""
Improve the getArtist method. Ensures IDs are integers.
"""
def _albums_iterator(albums):
for album in force_list(albums):
album["id"] = int(album["id"])
if "artistId" in album:
album["artistId"] = int(album["artistId"])
yield album
response = super(SubsonicClient, self).getArtist(*args, **kwargs)
response["artist"]["album"] = list(
_albums_iterator(response["artist"].get("album")))
return response
def getMusicDirectory(self, *args, **kwargs):
"""
Improve the getMusicDirectory method. Ensures IDs are integers.
"""
def _children_iterator(children):
for child in force_list(children):
child["id"] = int(child["id"])
if "parent" in child:
child["parent"] = int(child["parent"])
if "coverArt" in child:
child["coverArt"] = int(child["coverArt"])
if "artistId" in child:
child["artistId"] = int(child["artistId"])
if "albumId" in child:
child["albumId"] = int(child["albumId"])
yield child
response = super(SubsonicClient, self).getMusicDirectory(
*args, **kwargs)
response["directory"]["child"] = list(
_children_iterator(response["directory"].get("child")))
return response
def getAlbum(self, *args, **kwargs):
"""
Improve the getAlbum method. Ensures the IDs are real integers.
"""
def _songs_iterator(songs):
for song in force_list(songs):
song["id"] = int(song["id"])
yield song
response = super(SubsonicClient, self).getAlbum(*args, **kwargs)
response["album"]["song"] = list(
_songs_iterator(response["album"].get("song")))
return response
def getAlbumList2(self, *args, **kwargs):
"""
Improve the getAlbumList2 method. Ensures the IDs are real integers.
"""
def _album_iterator(albums):
for album in force_list(albums):
album["id"] = int(album["id"])
yield album
response = super(SubsonicClient, self).getAlbumList2(*args, **kwargs)
response["albumList2"]["album"] = list(
_album_iterator(response["albumList2"].get("album")))
return response
def getStarred(self, *args, **kwargs):
"""
Improve the getStarred method. Ensures the IDs are real integers.
"""
def _song_iterator(songs):
for song in force_list(songs):
song["id"] = int(song["id"])
yield song
response = super(SubsonicClient, self).getStarred(*args, **kwargs)
response["starred"]["song"] = list(
_song_iterator(response["starred"].get("song")))
return response
def getCoverArtUrl(self, *args, **kwargs):
"""
Return an URL to the cover art.
"""
self.intercept_url = True
url = self.getCoverArt(*args, **kwargs)
self.intercept_url = False
return url
def streamUrl(self, *args, **kwargs):
"""
Return an URL to the file to stream.
"""
self.intercept_url = True
url = self.stream(*args, **kwargs)
self.intercept_url = False
return url
def _doBinReq(self, *args, **kwargs):
"""
Intercept request URL to provide the URL of the item that is requested.
If the URL is intercepted, the request is not executed. A username and
password is added to provide direct access to the stream.
"""
if self.intercept_url:
parts = list(urlparse.urlparse(
args[0].get_full_url() + "?" + args[0].data))
parts[4] = dict(urlparse.parse_qsl(parts[4]))
parts[4].update({"u": self.username, "p": self.password})
parts[4] = urllib.urlencode(parts[4])
return urlparse.urlunparse(parts)
else:
return super(SubsonicClient, self)._doBinReq(*args, **kwargs)
def walk_index(self):
"""
Request Subsonic's index and iterate each item.
"""
response = self.getIndexes()
for index in response["indexes"]["index"]:
for index in index["artist"]:
for item in self.walk_directory(index["id"]):
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):
"""
Request Subsonic's playlists and iterate over each item.
"""
response = self.getPlaylists()
for child in response["playlists"]["playlist"]:
yield child
def walk_playlist(self, playlist_id):
"""
Request Subsonic's playlist items and iterate over each item.
"""
response = self.getPlaylist(playlist_id)
for child in response["playlist"]["entry"]:
yield child
def walk_directory(self, directory_id):
"""
Request a Subsonic music directory and iterate over each item.
"""
response = self.getMusicDirectory(directory_id)
for child in response["directory"]["child"]:
if child.get("isDir"):
for child in self.walk_directory(child["id"]):
yield child
else:
yield child
def walk_artist(self, artist_id):
"""
Request a Subsonic artist and iterate over each album.
"""
response = self.getArtist(artist_id)
for child in response["artist"]["album"]:
yield child
def walk_artists(self):
"""
Request all artists and iterate over each item.
"""
response = self.getArtists()
for index in response["artists"]["index"]:
for artist in index["artist"]:
yield artist
def walk_genres(self):
"""
Request all genres and iterate over each item.
"""
response = self.getGenres()
for genre in response["genres"]["genre"]:
yield genre
def walk_albums(self, ltype, size=None, fromYear=None,toYear=None, genre=None, offset=None):
"""
Request all albums for a given genre and iterate over each album.
"""
if ltype == 'byGenre' and genre is None:
return
if ltype == 'byYear' and (fromYear is None or toYear is None):
return
response = self.getAlbumList2(
ltype=ltype, size=size, fromYear=fromYear, toYear=toYear,genre=genre, offset=offset)
if not response["albumList2"]["album"]:
return
for album in response["albumList2"]["album"]:
yield album
def walk_album(self, album_id):
"""
Request an alum and iterate over each item.
"""
response = self.getAlbum(album_id)
for song in response["album"]["song"]:
yield song
def walk_tracks_random(self, size=None, genre=None, fromYear=None,toYear=None):
"""
Request random songs by genre and/or year and iterate over each song.
"""
response = self.getRandomSongs(
size=size, genre=genre, fromYear=fromYear, toYear=toYear)
for song in response["randomSongs"]["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

View File

@ -1,4 +1,4 @@
#v2.0.1
#v2.1.0
#https://github.com/romanvm/script.module.simpleplugin/releases
from simpleplugin import *
from .simpleplugin import *

File diff suppressed because it is too large Load Diff

1449
main.py

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,171 @@
# 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 ""
msgctxt "#30039"
msgid "Search"
msgstr ""
msgctxt "#30040"
msgid "useGET"
msgstr ""
msgctxt "#30041"
msgid "legacyauth"
msgstr ""
msgctxt "#30042"
msgid "port"
msgstr ""

View File

@ -0,0 +1,171 @@
# 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"
msgctxt "#30039"
msgid "Search"
msgstr "Rechercher"
msgctxt "#30040"
msgid "useGET"
msgstr ""
msgctxt "#30041"
msgid "legacyauth"
msgstr ""
msgctxt "#30042"
msgid "port"
msgstr ""

View File

@ -0,0 +1,170 @@
# 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"
msgctxt "#30039"
msgid "Search"
msgstr "Suche"
msgctxt "#30040"
msgid "useGET"
msgstr ""
msgctxt "#30041"
msgid "legacyauth"
msgstr ""
msgctxt "#30042"
msgid "port"
msgstr ""

View File

@ -1,27 +1,31 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings>
<!-- 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="Download" type="lsep" />
<setting id="download_folder" type="folder" label="Download folder" source="auto" option="writeable"/>
<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 label="30000">
<setting label="30001" type="lsep" />
<setting label="30002" id="subsonic_url" type="text" default="http://demo.subsonic.org"/>
<setting label="30042" id="port" type="text" default="80"/>
<setting label="30003" id="username" type="text" default="guest3"/>
<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" default="0" values="320|256|224|192|160|128|112|96|80|64|56|48|40|32|0"/>
</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 (in minutes) - not yet implemented" type="lsep" />
<setting id="cachetime" type="labelenum" label="Cache datas time" default="5" values="1|5|15|30|60|120|180|720|1440"/>
<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|1.15.0|1.16.0" default="1.15.0"/>
<setting label="30016" id="insecure" type="bool" default="false" />
<setting label="30040" id="useget" type="bool" default="true" />
<setting label="30041" id="legacyauth" 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>