diff --git a/lib/slskd_api/__init__.py b/lib/slskd_api/__init__.py
new file mode 100644
index 000000000..49671a419
--- /dev/null
+++ b/lib/slskd_api/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .client import SlskdClient, MetricsApi
+
+__all__ = ('SlskdClient', 'MetricsApi')
\ No newline at end of file
diff --git a/lib/slskd_api/apis/__init__.py b/lib/slskd_api/apis/__init__.py
new file mode 100644
index 000000000..5c7494123
--- /dev/null
+++ b/lib/slskd_api/apis/__init__.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .application import ApplicationApi
+from .conversations import ConversationsApi
+from .logs import LogsApi
+from .options import OptionsApi
+from .public_chat import PublicChatApi
+from .relay import RelayApi
+from .rooms import RoomsApi
+from .searches import SearchesApi
+from .server import ServerApi
+from .session import SessionApi
+from .shares import SharesApi
+from .transfers import TransfersApi
+from .users import UsersApi
+
+__all__ = (
+ 'ApplicationApi',
+ 'ConversationsApi',
+ 'LogsApi',
+ 'OptionsApi',
+ 'PublicChatApi',
+ 'RelayApi',
+ 'RoomsApi',
+ 'SearchesApi',
+ 'ServerApi',
+ 'SessionApi',
+ 'SharesApi',
+ 'TransfersApi',
+ 'UsersApi'
+)
diff --git a/lib/slskd_api/apis/application.py b/lib/slskd_api/apis/application.py
new file mode 100644
index 000000000..aa79f4a8d
--- /dev/null
+++ b/lib/slskd_api/apis/application.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class ApplicationApi(BaseApi):
+ """
+ This class contains the methods to interact with the Application API.
+ """
+
+ def state(self) -> dict:
+ """
+ Gets the current state of the application.
+ """
+ url = self.api_url + '/application'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def stop(self) -> bool:
+ """
+ Stops the application. Only works with token (usr/pwd login). 'Unauthorized' with API-Key.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/application'
+ response = self.session.delete(url)
+ return response.ok
+
+
+ def restart(self) -> bool:
+ """
+ Restarts the application. Only works with token (usr/pwd login). 'Unauthorized' with API-Key.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/application'
+ response = self.session.put(url)
+ return response.ok
+
+
+ def version(self) -> str:
+ """
+ Gets the current application version.
+ """
+ url = self.api_url + '/application/version'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def check_updates(self, forceCheck: bool = False) -> dict:
+ """
+ Checks for updates.
+ """
+ url = self.api_url + '/application/version/latest'
+ params = dict(
+ forceCheck=forceCheck
+ )
+ response = self.session.get(url, params=params)
+ return response.json()
+
+
+ def gc(self) -> bool:
+ """
+ Forces garbage collection.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/application/gc'
+ response = self.session.post(url)
+ return response.ok
+
+
+# Not supposed to be part of the external API
+# More info in the Github discussion: https://github.com/slskd/slskd/discussions/910
+ # def dump(self):
+ # url = self.api_url + '/application/dump'
+ # response = self.session.get(url)
+ # return response.json()
\ No newline at end of file
diff --git a/lib/slskd_api/apis/base.py b/lib/slskd_api/apis/base.py
new file mode 100644
index 000000000..7a2ace31a
--- /dev/null
+++ b/lib/slskd_api/apis/base.py
@@ -0,0 +1,26 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import requests
+from urllib.parse import quote
+
+class BaseApi:
+ """
+ Base class where api-url and headers are set for all requests.
+ """
+
+ def __init__(self, api_url: str, session: requests.Session):
+ self.api_url = api_url
+ self.session = session
\ No newline at end of file
diff --git a/lib/slskd_api/apis/conversations.py b/lib/slskd_api/apis/conversations.py
new file mode 100644
index 000000000..43557352d
--- /dev/null
+++ b/lib/slskd_api/apis/conversations.py
@@ -0,0 +1,103 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class ConversationsApi(BaseApi):
+ """
+ This class contains the methods to interact with the Conversations API.
+ """
+
+ def acknowledge(self, username: str, id: int) -> bool:
+ """
+ Acknowledges the given message id for the given username.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/conversations/{quote(username)}/{id}'
+ response = self.session.put(url)
+ return response.ok
+
+
+ def acknowledge_all(self, username: str) -> bool:
+ """
+ Acknowledges all messages from the given username.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/conversations/{quote(username)}'
+ response = self.session.put(url)
+ return response.ok
+
+
+ def delete(self, username: str) -> bool:
+ """
+ Closes the conversation associated with the given username.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/conversations/{quote(username)}'
+ response = self.session.delete(url)
+ return response.ok
+
+
+ def get(self, username: str, includeMessages: bool = True) -> dict:
+ """
+ Gets the conversation associated with the specified username.
+ """
+ url = self.api_url + f'/conversations/{quote(username)}'
+ params = dict(
+ includeMessages=includeMessages
+ )
+ response = self.session.get(url, params=params)
+ return response.json()
+
+
+ def send(self, username: str, message: str) -> bool:
+ """
+ Sends a private message to the specified username.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/conversations/{quote(username)}'
+ response = self.session.post(url, json=message)
+ return response.ok
+
+
+ def get_all(self, includeInactive: bool = False, unAcknowledgedOnly : bool = False) -> list:
+ """
+ Gets all active conversations.
+ """
+ url = self.api_url + '/conversations'
+ params = dict(
+ includeInactive=includeInactive,
+ unAcknowledgedOnly=unAcknowledgedOnly
+ )
+ response = self.session.get(url, params=params)
+ return response.json()
+
+
+ def get_messages(self, username: str, unAcknowledgedOnly : bool = False) -> list:
+ """
+ Gets all messages associated with the specified username.
+ """
+ url = self.api_url + f'/conversations/{quote(username)}/messages'
+ params = dict(
+ username=username,
+ unAcknowledgedOnly=unAcknowledgedOnly
+ )
+ response = self.session.get(url, params=params)
+ return response.json()
+
\ No newline at end of file
diff --git a/lib/slskd_api/apis/logs.py b/lib/slskd_api/apis/logs.py
new file mode 100644
index 000000000..0248e3abd
--- /dev/null
+++ b/lib/slskd_api/apis/logs.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class LogsApi(BaseApi):
+ """
+ This class contains the methods to interact with the Logs API.
+ """
+
+ def get(self) -> list:
+ """
+ Gets the last few application logs.
+ """
+ url = self.api_url + '/logs'
+ response = self.session.get(url)
+ return response.json()
\ No newline at end of file
diff --git a/lib/slskd_api/apis/options.py b/lib/slskd_api/apis/options.py
new file mode 100644
index 000000000..30f655ade
--- /dev/null
+++ b/lib/slskd_api/apis/options.py
@@ -0,0 +1,93 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class OptionsApi(BaseApi):
+ """
+ This class contains the methods to interact with the Options API.
+ """
+
+ def get(self) -> dict:
+ """
+ Gets the current application options.
+ """
+ url = self.api_url + '/options'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def get_startup(self) -> dict:
+ """
+ Gets the application options provided at startup.
+ """
+ url = self.api_url + '/options/startup'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def debug(self) -> str:
+ """
+ Gets the debug view of the current application options.
+ debug and remote_configuration must be set to true.
+ Only works with token (usr/pwd login). 'Unauthorized' with API-Key.
+ """
+ url = self.api_url + '/options/debug'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def yaml_location(self) -> str:
+ """
+ Gets the path of the yaml config file. remote_configuration must be set to true.
+ Only works with token (usr/pwd login). 'Unauthorized' with API-Key.
+ """
+ url = self.api_url + '/options/yaml/location'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def download_yaml(self) -> str:
+ """
+ Gets the content of the yaml config file as text. remote_configuration must be set to true.
+ Only works with token (usr/pwd login). 'Unauthorized' with API-Key.
+ """
+ url = self.api_url + '/options/yaml'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def upload_yaml(self, yaml_content: str) -> bool:
+ """
+ Sets the content of the yaml config file. remote_configuration must be set to true.
+ Only works with token (usr/pwd login). 'Unauthorized' with API-Key.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/options/yaml'
+ response = self.session.post(url, json=yaml_content)
+ return response.ok
+
+
+ def validate_yaml(self, yaml_content: str) -> str:
+ """
+ Validates the provided yaml string. remote_configuration must be set to true.
+ Only works with token (usr/pwd login). 'Unauthorized' with API-Key.
+
+ :return: Empty string if validation successful. Error message otherwise.
+ """
+ url = self.api_url + '/options/yaml/validate'
+ response = self.session.post(url, json=yaml_content)
+ return response.text
\ No newline at end of file
diff --git a/lib/slskd_api/apis/public_chat.py b/lib/slskd_api/apis/public_chat.py
new file mode 100644
index 000000000..0dc17ff35
--- /dev/null
+++ b/lib/slskd_api/apis/public_chat.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class PublicChatApi(BaseApi):
+ """
+ [UNTESTED] This class contains the methods to interact with the PublicChat API.
+ """
+
+ def start(self) -> bool:
+ """
+ Starts public chat.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/publicchat'
+ response = self.session.post(url)
+ return response.ok
+
+
+ def stop(self) -> bool:
+ """
+ Stops public chat.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/publicchat'
+ response = self.session.delete(url)
+ return response.ok
diff --git a/lib/slskd_api/apis/relay.py b/lib/slskd_api/apis/relay.py
new file mode 100644
index 000000000..bd006f2ad
--- /dev/null
+++ b/lib/slskd_api/apis/relay.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class RelayApi(BaseApi):
+ """
+ [UNTESTED] This class contains the methods to interact with the Relay API.
+ """
+
+ def connect(self) -> bool:
+ """
+ Connects to the configured controller.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/relay/agent'
+ response = self.session.put(url)
+ return response.ok
+
+
+ def disconnect(self) -> bool:
+ """
+ Disconnects from the connected controller.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/relay/agent'
+ response = self.session.delete(url)
+ return response.ok
+
+
+ def download_file(self, token: str) -> bool:
+ """
+ Downloads a file from the connected controller.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/relay/controller/downloads/{token}'
+ response = self.session.get(url)
+ return response.ok
+
+
+ def upload_file(self, token: str) -> bool:
+ """
+ Uploads a file from the connected controller.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/relay/controller/files/{token}'
+ response = self.session.post(url)
+ return response.ok
+
+
+ def upload_share_info(self, token: str) -> bool:
+ """
+ Uploads share information to the connected controller.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/relay/controller/shares/{token}'
+ response = self.session.post(url)
+ return response.ok
\ No newline at end of file
diff --git a/lib/slskd_api/apis/rooms.py b/lib/slskd_api/apis/rooms.py
new file mode 100644
index 000000000..98fb19332
--- /dev/null
+++ b/lib/slskd_api/apis/rooms.py
@@ -0,0 +1,124 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class RoomsApi(BaseApi):
+ """
+ This class contains the methods to interact with the Rooms API.
+ """
+
+ def get_all_joined(self) -> list:
+ """
+ Gets all joined rooms.
+
+ :return: Names of the joined rooms.
+ """
+ url = self.api_url + '/rooms/joined'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def join(self, roomName: str) -> dict:
+ """
+ Joins a room.
+
+ :return: room info: name, isPrivate, users, messages
+ """
+ url = self.api_url + '/rooms/joined'
+ response = self.session.post(url, json=roomName)
+ return response.json()
+
+
+ def get_joined(self, roomName: str) -> dict:
+ """
+ Gets the specified room.
+
+ :return: room info: name, isPrivate, users, messages
+ """
+ url = self.api_url + f'/rooms/joined/{quote(roomName)}'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def leave(self, roomName: str) -> bool:
+ """
+ Leaves a room.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/rooms/joined/{quote(roomName)}'
+ response = self.session.delete(url)
+ return response.ok
+
+
+ def send(self, roomName: str, message: str) -> bool:
+ """
+ Sends a message to the specified room.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/rooms/joined/{quote(roomName)}/messages'
+ response = self.session.post(url, json=message)
+ return response.ok
+
+
+ def get_messages(self, roomName: str) -> list:
+ """
+ Gets the current list of messages for the specified room.
+ """
+ url = self.api_url + f'/rooms/joined/{quote(roomName)}/messages'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def set_ticker(self, roomName: str, ticker: str) -> bool:
+ """
+ Sets a ticker for the specified room.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/rooms/joined/{quote(roomName)}/ticker'
+ response = self.session.post(url, json=ticker)
+ return response.ok
+
+
+ def add_member(self, roomName: str, username: str) -> bool:
+ """
+ Adds a member to a private room.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/rooms/joined/{quote(roomName)}/members'
+ response = self.session.post(url, json=username)
+ return response.ok
+
+
+ def get_users(self, roomName: str) -> list:
+ """
+ Gets the current list of users for the specified joined room.
+ """
+ url = self.api_url + f'/rooms/joined/{quote(roomName)}/users'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def get_all(self) -> list:
+ """
+ Gets a list of rooms from the server.
+ """
+ url = self.api_url + '/rooms/available'
+ response = self.session.get(url)
+ return response.json()
\ No newline at end of file
diff --git a/lib/slskd_api/apis/searches.py b/lib/slskd_api/apis/searches.py
new file mode 100644
index 000000000..1f521c725
--- /dev/null
+++ b/lib/slskd_api/apis/searches.py
@@ -0,0 +1,126 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+import uuid
+from typing import Optional
+
+class SearchesApi(BaseApi):
+ """
+ Class that handles operations on searches.
+ """
+
+ def search_text(self,
+ searchText: str,
+ id: Optional[str] = None,
+ fileLimit: int = 10000,
+ filterResponses: bool = True,
+ maximumPeerQueueLength: int = 1000000,
+ minimumPeerUploadSpeed: int = 0,
+ minimumResponseFileCount: int = 1,
+ responseLimit: int = 100,
+ searchTimeout: int = 15000
+ ) -> dict:
+ """
+ Performs a search for the specified request.
+
+ :param searchText: Search query
+ :param id: uuid of the search. One will be generated if None.
+ :param fileLimit: Max number of file results
+ :param filterResponses: Filter unreachable users from the results
+ :param maximumPeerQueueLength: Max queue length
+ :param minimumPeerUploadSpeed: Min upload speed in bit/s
+ :param minimumResponseFileCount: Min number of matching files per user
+ :param responseLimit: Max number of users results
+ :param searchTimeout: Search timeout in ms
+ :return: Info about the search (no results!)
+ """
+
+ url = self.api_url + '/searches'
+
+ try:
+ id = str(uuid.UUID(id)) # check if given id is a valid uuid
+ except:
+ id = str(uuid.uuid1()) # otherwise generate a new one
+
+ data = {
+ "id": id,
+ "fileLimit": fileLimit,
+ "filterResponses": filterResponses,
+ "maximumPeerQueueLength": maximumPeerQueueLength,
+ "minimumPeerUploadSpeed": minimumPeerUploadSpeed,
+ "minimumResponseFileCount": minimumResponseFileCount,
+ "responseLimit": responseLimit,
+ "searchText": searchText,
+ "searchTimeout": searchTimeout,
+ }
+ response = self.session.post(url, json=data)
+ return response.json()
+
+
+ def get_all(self) -> list:
+ """
+ Gets the list of active and completed searches.
+ """
+ url = self.api_url + '/searches'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def state(self, id: str, includeResponses: bool = False) -> dict:
+ """
+ Gets the state of the search corresponding to the specified id.
+
+ :param id: uuid of the search.
+ :param includeResponses: Include responses (search result list) in the returned dict
+ :return: Info about the search
+ """
+ url = self.api_url + f'/searches/{id}'
+ params = dict(
+ includeResponses=includeResponses
+ )
+ response = self.session.get(url, params=params)
+ return response.json()
+
+
+ def stop(self, id: str) -> bool:
+ """
+ Stops the search corresponding to the specified id.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/searches/{id}'
+ response = self.session.put(url)
+ return response.ok
+
+
+ def delete(self, id: str):
+ """
+ Deletes the search corresponding to the specified id.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/searches/{id}'
+ response = self.session.delete(url)
+ return response.ok
+
+
+ def search_responses(self, id: str) -> list:
+ """
+ Gets search responses corresponding to the specified id.
+ """
+ url = self.api_url + f'/searches/{id}/responses'
+ response = self.session.get(url)
+ return response.json()
\ No newline at end of file
diff --git a/lib/slskd_api/apis/server.py b/lib/slskd_api/apis/server.py
new file mode 100644
index 000000000..5365dde1a
--- /dev/null
+++ b/lib/slskd_api/apis/server.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class ServerApi(BaseApi):
+ """
+ This class contains the methods to interact with the Server API.
+ """
+
+ def connect(self) -> bool:
+ """
+ Connects the client.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/server'
+ response = self.session.put(url)
+ return response.ok
+
+
+ def disconnect(self) -> bool:
+ """
+ Disconnects the client.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/server'
+ response = self.session.delete(url, json='')
+ return response.ok
+
+
+ def state(self) -> dict:
+ """
+ Retrieves the current state of the server.
+ """
+ url = self.api_url + '/server'
+ response = self.session.get(url)
+ return response.json()
\ No newline at end of file
diff --git a/lib/slskd_api/apis/session.py b/lib/slskd_api/apis/session.py
new file mode 100644
index 000000000..76e28ed46
--- /dev/null
+++ b/lib/slskd_api/apis/session.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class SessionApi(BaseApi):
+ """
+ This class contains the methods to interact with the Session API.
+ """
+
+ def auth_valid(self) -> bool:
+ """
+ Checks whether the provided authentication is valid.
+ """
+ url = self.api_url + '/session'
+ response = self.session.get(url)
+ return response.ok
+
+
+ def login(self, username: str, password: str) -> dict:
+ """
+ Logs in.
+
+ :return: Session info for the given user incl. token.
+ """
+ url = self.api_url + '/session'
+ data = {
+ 'username': username,
+ 'password': password
+ }
+ response = self.session.post(url, json=data)
+ return response.json()
+
+
+ def security_enabled(self) -> bool:
+ """
+ Checks whether security is enabled.
+ """
+ url = self.api_url + '/session/enabled'
+ response = self.session.get(url)
+ return response.json()
\ No newline at end of file
diff --git a/lib/slskd_api/apis/shares.py b/lib/slskd_api/apis/shares.py
new file mode 100644
index 000000000..2e92642dc
--- /dev/null
+++ b/lib/slskd_api/apis/shares.py
@@ -0,0 +1,78 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class SharesApi(BaseApi):
+ """
+ This class contains the methods to interact with the Shares API.
+ """
+
+ def get_all(self) -> dict:
+ """
+ Gets the current list of shares.
+ """
+ url = self.api_url + '/shares'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def start_scan(self) -> bool:
+ """
+ Initiates a scan of the configured shares.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/shares'
+ response = self.session.put(url)
+ return response.ok
+
+
+ def cancel_scan(self) -> bool:
+ """
+ Cancels a share scan, if one is running.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/shares'
+ response = self.session.delete(url)
+ return response.ok
+
+
+ def get(self, id: str) -> dict:
+ """
+ Gets the share associated with the specified id.
+ """
+ url = self.api_url + f'/shares/{id}'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def all_contents(self) -> list:
+ """
+ Returns a list of all shared directories and files.
+ """
+ url = self.api_url + '/shares/contents'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def contents(self, id: str) -> list:
+ """
+ Gets the contents of the share associated with the specified id.
+ """
+ url = self.api_url + f'/shares/{id}/contents'
+ response = self.session.get(url)
+ return response.json()
\ No newline at end of file
diff --git a/lib/slskd_api/apis/transfers.py b/lib/slskd_api/apis/transfers.py
new file mode 100644
index 000000000..6bbae5448
--- /dev/null
+++ b/lib/slskd_api/apis/transfers.py
@@ -0,0 +1,157 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+from typing import Union
+
+class TransfersApi(BaseApi):
+ """
+ This class contains the methods to interact with the Transfers API.
+ """
+
+ def cancel_download(self, username: str, id:str, remove: bool = False) -> bool:
+ """
+ Cancels the specified download.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/transfers/downloads/{quote(username)}/{id}'
+ params = dict(
+ remove=remove
+ )
+ response = self.session.delete(url, params=params)
+ return response.ok
+
+
+ def get_download(self, username: str, id: str) -> dict:
+ """
+ Gets the specified download.
+ """
+ url = self.api_url + f'/transfers/downloads/{quote(username)}/{id}'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def remove_completed_downloads(self) -> bool:
+ """
+ Removes all completed downloads, regardless of whether they failed or succeeded.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/transfers/downloads/all/completed'
+ response = self.session.delete(url)
+ return response.ok
+
+
+ def cancel_upload(self, username: str, id: str, remove: bool = False) -> bool:
+ """
+ Cancels the specified upload.
+
+ :return: True if successful.
+ """
+ url = self.api_url + f'/transfers/uploads/{quote(username)}/{id}'
+ params = dict(
+ remove=remove
+ )
+ response = self.session.delete(url, params=params)
+ return response.ok
+
+
+ def get_upload(self, username: str, id: str) -> dict:
+ """
+ Gets the specified upload.
+ """
+ url = self.api_url + f'/transfers/uploads/{quote(username)}/{id}'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def remove_completed_uploads(self) -> bool:
+ """
+ Removes all completed uploads, regardless of whether they failed or succeeded.
+
+ :return: True if successful.
+ """
+ url = self.api_url + '/transfers/uploads/all/completed'
+ response = self.session.delete(url)
+ return response.ok
+
+
+ def enqueue(self, username: str, files: list) -> bool:
+ """
+ Enqueues the specified download.
+
+ :param username: User to download from.
+ :param files: A list of dictionaries in the same form as what's returned
+ by :py:func:`~slskd_api.apis.SearchesApi.search_responses`:
+ [{'filename': , 'size': }...]
+ :return: True if successful.
+ """
+ url = self.api_url + f'/transfers/downloads/{quote(username)}'
+ response = self.session.post(url, json=files)
+ return response.ok
+
+
+ def get_downloads(self, username: str) -> dict:
+ """
+ Gets all downloads for the specified username.
+ """
+ url = self.api_url + f'/transfers/downloads/{quote(username)}'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def get_all_downloads(self, includeRemoved: bool = False) -> list:
+ """
+ Gets all downloads.
+ """
+ url = self.api_url + '/transfers/downloads/'
+ params = dict(
+ includeRemoved=includeRemoved
+ )
+ response = self.session.get(url, params=params)
+ return response.json()
+
+
+ def get_queue_position(self, username: str, id: str) -> Union[int,str]:
+ """
+ Gets the download for the specified username matching the specified filename, and requests the current place in the remote queue of the specified download.
+
+ :return: Queue position or error message
+ """
+ url = self.api_url + f'/transfers/downloads/{quote(username)}/{id}/position'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def get_all_uploads(self, includeRemoved: bool = False) -> list:
+ """
+ Gets all uploads.
+ """
+ url = self.api_url + '/transfers/uploads/'
+ params = dict(
+ includeRemoved=includeRemoved
+ )
+ response = self.session.get(url, params=params)
+ return response.json()
+
+
+ def get_uploads(self, username: str) -> dict:
+ """
+ Gets all uploads for the specified username.
+ """
+ url = self.api_url + f'/transfers/uploads/{quote(username)}'
+ response = self.session.get(url)
+ return response.json()
\ No newline at end of file
diff --git a/lib/slskd_api/apis/users.py b/lib/slskd_api/apis/users.py
new file mode 100644
index 000000000..82a1b9bf0
--- /dev/null
+++ b/lib/slskd_api/apis/users.py
@@ -0,0 +1,79 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from .base import *
+
+class UsersApi(BaseApi):
+ """
+ This class contains the methods to interact with the Users API.
+ """
+
+ def address(self, username: str) -> dict:
+ """
+ Retrieves the address of the specified username.
+ """
+ url = self.api_url + f'/users/{quote(username)}/endpoint'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def browse(self, username: str) -> dict:
+ """
+ Retrieves the files shared by the specified username.
+ """
+ url = self.api_url + f'/users/{quote(username)}/browse'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def browsing_status(self, username: str) -> dict:
+ """
+ Retrieves the status of the current browse operation for the specified username, if any.
+ Will return error 404 if called after the browsing operation has ended.
+ Best called asynchronously while :py:func:`browse` is still running.
+ """
+ url = self.api_url + f'/users/{quote(username)}/browse/status'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def directory(self, username: str, directory: str) -> dict:
+ """
+ Retrieves the files from the specified directory from the specified username.
+ """
+ url = self.api_url + f'/users/{quote(username)}/directory'
+ data = {
+ "directory": directory
+ }
+ response = self.session.post(url, json=data)
+ return response.json()
+
+
+ def info(self, username: str) -> dict:
+ """
+ Retrieves information about the specified username.
+ """
+ url = self.api_url + f'/users/{quote(username)}/info'
+ response = self.session.get(url)
+ return response.json()
+
+
+ def status(self, username: str) -> dict:
+ """
+ Retrieves status for the specified username.
+ """
+ url = self.api_url + f'/users/{quote(username)}/status'
+ response = self.session.get(url)
+ return response.json()
\ No newline at end of file
diff --git a/lib/slskd_api/client.py b/lib/slskd_api/client.py
new file mode 100644
index 000000000..961a68390
--- /dev/null
+++ b/lib/slskd_api/client.py
@@ -0,0 +1,123 @@
+# Copyright (C) 2023 bigoulours
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+API_VERSION = 'v0'
+
+import requests
+from urllib.parse import urljoin
+from functools import reduce
+from base64 import b64encode
+from slskd_api.apis import *
+
+
+class HTTPAdapterTimeout(requests.adapters.HTTPAdapter):
+ def __init__(self, timeout=None, **kwargs):
+ super().__init__(**kwargs)
+ self.timeout = timeout
+
+ def send(self, *args, **kwargs):
+ kwargs['timeout'] = self.timeout
+ return super().send(*args, **kwargs)
+
+
+class SlskdClient:
+ """
+ The main class that allows access to the different APIs of a slskd instance.
+ An API-Key with appropriate permissions (`readwrite` for most use cases) must be set in slskd config file.
+ Alternatively, provide your username and password. Requests error status raise corresponding error.
+ Usage::
+ slskd = slskd_api.SlskdClient(host, api_key, url_base)
+ app_status = slskd.application.state()
+ """
+
+ def __init__(self,
+ host: str,
+ api_key: str = None,
+ url_base: str = '/',
+ username: str = None,
+ password: str = None,
+ token: str = None,
+ verify_ssl: bool = True,
+ timeout: float = None # requests timeout in seconds
+ ):
+ api_url = reduce(urljoin, [f'{host}/', f'{url_base}/', f'api/{API_VERSION}'])
+
+ session = requests.Session()
+ session.adapters['http://'] = HTTPAdapterTimeout(timeout=timeout)
+ session.adapters['https://'] = HTTPAdapterTimeout(timeout=timeout)
+ session.hooks = {'response': lambda r, *args, **kwargs: r.raise_for_status()}
+ session.headers.update({'accept': '*/*'})
+ session.verify = verify_ssl
+
+ header = {}
+
+ if api_key:
+ header['X-API-Key'] = api_key
+ elif username and password:
+ header['Authorization'] = 'Bearer ' + \
+ SessionApi(api_url, session).login(username, password).get('token', '')
+ elif token:
+ header['Authorization'] = 'Bearer ' + token
+ else:
+ raise ValueError('Please provide an API-Key, a valid token or username/password.')
+
+ session.headers.update(header)
+
+ base_args = (api_url, session)
+
+ self.application = ApplicationApi(*base_args)
+ self.conversations = ConversationsApi(*base_args)
+ self.logs = LogsApi(*base_args)
+ self.options = OptionsApi(*base_args)
+ self.public_chat = PublicChatApi(*base_args)
+ self.relay = RelayApi(*base_args)
+ self.rooms = RoomsApi(*base_args)
+ self.searches = SearchesApi(*base_args)
+ self.server = ServerApi(*base_args)
+ self.session = SessionApi(*base_args)
+ self.shares = SharesApi(*base_args)
+ self.transfers = TransfersApi(*base_args)
+ self.users = UsersApi(*base_args)
+
+
+class MetricsApi:
+ """
+ Getting the metrics works with a different endpoint. Default: :5030/metrics.
+ Metrics should be first activated in slskd config file.
+ User/pass is independent from the main application and default value (slskd:slskd) should be changed.
+ Usage::
+ metrics_api = slskd_api.MetricsApi(host, metrics_usr='slskd', metrics_pwd='slskd')
+ metrics = metrics_api.get()
+ """
+
+ def __init__(self,
+ host: str,
+ metrics_usr: str = 'slskd',
+ metrics_pwd: str = 'slskd',
+ metrics_url_base: str = '/metrics'
+ ):
+ self.metrics_url = urljoin(host, metrics_url_base)
+ basic_auth = b64encode(bytes(f'{metrics_usr}:{metrics_pwd}', 'utf-8'))
+ self.header = {
+ 'accept': '*/*',
+ 'Authorization': f'Basic {basic_auth.decode()}'
+ }
+
+ def get(self) -> str:
+ """
+ Gets the Prometheus metrics as text.
+ """
+ response = requests.get(self.metrics_url, headers=self.header)
+ return response.text