diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py
index fdeebf1..1b441cc 100644
--- a/supysonic/api/__init__.py
+++ b/supysonic/api/__init__.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API.
#
-# Copyright (C) 2013-2023 Alban 'spl0k' Féron
+# Copyright (C) 2013-2024 Alban 'spl0k' Féron
#
# Distributed under terms of the GNU AGPLv3 license.
@@ -152,3 +152,4 @@ from .jukebox import *
from .radio import *
from .unsupported import *
from .scan import *
+from .extensions import *
diff --git a/supysonic/api/extensions.py b/supysonic/api/extensions.py
new file mode 100644
index 0000000..4b1f505
--- /dev/null
+++ b/supysonic/api/extensions.py
@@ -0,0 +1,16 @@
+#
+# This file is part of Supysonic.
+# Supysonic is a Python implementation of the Subsonic server API.
+#
+# Copyright (C) 2024 Alban 'spl0k' Féron
+#
+# Distributed under terms of the GNU AGPLv3 license.
+
+from flask import request
+
+from . import api_routing
+
+
+@api_routing("/getOpenSubsonicExtensions")
+def extensions():
+ return request.formatter("openSubsonicExtensions", [])
diff --git a/supysonic/api/formatters.py b/supysonic/api/formatters.py
index a30de9c..7ac5073 100644
--- a/supysonic/api/formatters.py
+++ b/supysonic/api/formatters.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API.
#
-# Copyright (C) 2018 Alban 'spl0k' Féron
+# Copyright (C) 2018-2024 Alban 'spl0k' Féron
#
# Distributed under terms of the GNU AGPLv3 license.
@@ -9,6 +9,7 @@ from flask import json, jsonify, make_response
from xml.etree import ElementTree
from . import API_VERSION
+from .. import NAME, VERSION
class BaseFormatter:
@@ -59,7 +60,13 @@ class JSONBaseFormatter(BaseFormatter):
if (elem is None) != (data is None):
raise ValueError("Expecting both elem and data or neither of them")
- rv = {"status": "failed" if elem == "error" else "ok", "version": API_VERSION}
+ rv = {
+ "status": "failed" if elem == "error" else "ok",
+ "version": API_VERSION,
+ "type": NAME,
+ "serverVersion": VERSION,
+ "openSubsonic": True,
+ }
if data:
rv[elem] = self.__remove_empty_lists(data)
@@ -138,6 +145,9 @@ class XMLFormatter(BaseFormatter):
"status": "failed" if elem == "error" else "ok",
"version": API_VERSION,
"xmlns": "http://subsonic.org/restapi",
+ "type": NAME,
+ "serverVersion": VERSION,
+ "openSubsonic": True,
}
if elem:
response[elem] = data
diff --git a/tests/api/apitestbase.py b/tests/api/apitestbase.py
index 3531efe..c257cda 100644
--- a/tests/api/apitestbase.py
+++ b/tests/api/apitestbase.py
@@ -25,7 +25,7 @@ class ApiTestBase(TestBase):
super().setUp()
logging.getLogger("supysonic.api").addHandler(logging.NullHandler())
self.apiVersion = apiVersion
- xsd = etree.parse(f"tests/assets/subsonic-rest-api-{self.apiVersion}.xsd")
+ xsd = etree.parse(f"tests/assets/subsonic-rest-api-{self.apiVersion}-os.xsd")
self.schema = etree.XMLSchema(xsd)
def _find(self, xml, path):
diff --git a/tests/api/test_response_helper.py b/tests/api/test_response_helper.py
index 695b480..74e2626 100644
--- a/tests/api/test_response_helper.py
+++ b/tests/api/test_response_helper.py
@@ -1,7 +1,7 @@
# This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API.
#
-# Copyright (C) 2017-2018 Alban 'spl0k' Féron
+# Copyright (C) 2017-2024 Alban 'spl0k' Féron
#
# Distributed under terms of the GNU AGPLv3 license.
@@ -44,9 +44,12 @@ class ResponseHelperJsonTestCase(TestBase, UnwrapperMixin.create_from(JSONFormat
self.assertIsInstance(empty["subsonic-response"], dict)
resp = empty["subsonic-response"]
- self.assertEqual(len(resp), 2)
+ self.assertEqual(len(resp), 5)
self.assertIn("status", resp)
self.assertIn("version", resp)
+ self.assertIn("type", resp)
+ self.assertIn("serverVersion", resp)
+ self.assertIn("openSubsonic", resp)
self.assertEqual(resp["status"], "ok")
resp = self.error(0, "message")["subsonic-response"]
@@ -137,6 +140,9 @@ class ResponseHelperXMLTestCase(TestBase, UnwrapperMixin.create_from(XMLFormatte
empty = self.empty
self.assertIsNotNone(empty.find(".[@version]"))
self.assertIsNotNone(empty.find(".[@status='ok']"))
+ self.assertIsNotNone(empty.find(".[@type]"))
+ self.assertIsNotNone(empty.find(".[@serverVersion]"))
+ self.assertIsNotNone(empty.find(".[@openSubsonic='true']"))
resp = self.error(0, "message")
self.assertIsNotNone(resp.find(".[@status='failed']"))
diff --git a/tests/assets/subsonic-rest-api-1.12.0-os.xsd b/tests/assets/subsonic-rest-api-1.12.0-os.xsd
new file mode 100644
index 0000000..3994d24
--- /dev/null
+++ b/tests/assets/subsonic-rest-api-1.12.0-os.xsd
@@ -0,0 +1,566 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/assets/subsonic-rest-api-1.16.0-os.xsd b/tests/assets/subsonic-rest-api-1.16.0-os.xsd
new file mode 100644
index 0000000..433e64d
--- /dev/null
+++ b/tests/assets/subsonic-rest-api-1.16.0-os.xsd
@@ -0,0 +1,641 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file