/*** This file is part of snapcast Copyright (C) 2014-2025 Johannes Pohl This program 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. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #pragma once // local headers #include "common/json.hpp" #include "common/utils.hpp" #include "common/utils/string_utils.hpp" // standard headers #include #include #include #include #include namespace strutils = utils::string; using json = nlohmann::json; struct ClientInfo; struct Group; using ClientInfoPtr = std::shared_ptr; using GroupPtr = std::shared_ptr; /// Config item base class struct JsonConfigItem { /// Read config item from json object @p j virtual void fromJson(const json& j) = 0; /// @return config item serialized to json virtual json toJson() = 0; /// d'tor virtual ~JsonConfigItem() = default; protected: /// @return value for key @p what or @p def, if not found. Result is casted to T. template T jGet(const json& j, const std::string& what, const T& def) { try { if (!j.count(what)) return def; return j[what].get(); } catch (...) { return def; } } }; /// Volume config struct Volume : public JsonConfigItem { /// c'tor explicit Volume(uint16_t _percent = 100, bool _muted = false) : percent(_percent), muted(_muted) { } void fromJson(const json& j) override { percent = jGet(j, "percent", percent); muted = jGet(j, "muted", muted); } json toJson() override { json j; j["percent"] = percent; j["muted"] = muted; return j; } uint16_t percent; ///< volume in percent bool muted; ///< muted }; /// Host config struct Host : public JsonConfigItem { /// c'tor Host() = default; /// Update name, os and arch to actual values void update() { name = getHostName(); os = getOS(); arch = getArch(); } void fromJson(const json& j) override { name = strutils::trim_copy(jGet(j, "name", "")); mac = strutils::trim_copy(jGet(j, "mac", "")); os = strutils::trim_copy(jGet(j, "os", "")); arch = strutils::trim_copy(jGet(j, "arch", "")); ip = strutils::trim_copy(jGet(j, "ip", "")); } json toJson() override { json j; j["name"] = name; j["mac"] = mac; j["os"] = os; j["arch"] = arch; j["ip"] = ip; return j; } std::string name; ///< host name std::string mac; ///< mac address std::string os; ///< OS std::string arch; ///< CPU architecture std::string ip; ///< IP address }; /// Client config struct ClientConfig : public JsonConfigItem { /// c'tor ClientConfig() : volume(100) { } void fromJson(const json& j) override { name = strutils::trim_copy(jGet(j, "name", "")); volume.fromJson(j["volume"]); latency = jGet(j, "latency", 0); instance = jGet(j, "instance", 1); } json toJson() override { json j; j["name"] = strutils::trim_copy(name); j["volume"] = volume.toJson(); j["latency"] = latency; j["instance"] = instance; return j; } std::string name; ///< client name Volume volume; ///< client volume int32_t latency{0}; ///< additional latency size_t instance{1}; ///< instance id }; /// Snapcast base config struct Snapcast : public JsonConfigItem { /// c'tor explicit Snapcast(std::string _name = "", std::string _version = "") : name(std::move(_name)), version(std::move(_version)) { } virtual ~Snapcast() = default; void fromJson(const json& j) override { name = strutils::trim_copy(jGet(j, "name", "")); version = strutils::trim_copy(jGet(j, "version", "")); protocolVersion = jGet(j, "protocolVersion", 1); } json toJson() override { json j; j["name"] = strutils::trim_copy(name); j["version"] = strutils::trim_copy(version); j["protocolVersion"] = protocolVersion; return j; } std::string name; ///< name of the client or server std::string version; ///< software version int protocolVersion{1}; ///< binary protocol version }; /// Snapclient config struct Snapclient : public Snapcast { /// c'tor explicit Snapclient(std::string _name = "", std::string _version = "") : Snapcast(std::move(_name), std::move(_version)) { } }; /// Snapserver config struct Snapserver : public Snapcast { /// c'tor explicit Snapserver(std::string _name = "", std::string _version = "") : Snapcast(std::move(_name), std::move(_version)) { } void fromJson(const json& j) override { Snapcast::fromJson(j); controlProtocolVersion = jGet(j, "controlProtocolVersion", 1); } json toJson() override { json j = Snapcast::toJson(); j["controlProtocolVersion"] = controlProtocolVersion; return j; } int controlProtocolVersion{1}; ///< control protocol version }; /// Client config struct ClientInfo : public JsonConfigItem { /// c'tor explicit ClientInfo(std::string _clientId = "") : id(std::move(_clientId)) { lastSeen.tv_sec = 0; lastSeen.tv_usec = 0; } void fromJson(const json& j) override { host.fromJson(j["host"]); id = jGet(j, "id", host.mac); snapclient.fromJson(j["snapclient"]); config.fromJson(j["config"]); lastSeen.tv_sec = jGet(j["lastSeen"], "sec", 0); lastSeen.tv_usec = jGet(j["lastSeen"], "usec", 0); connected = jGet(j, "connected", true); } json toJson() override { json j; j["id"] = id; j["host"] = host.toJson(); j["snapclient"] = snapclient.toJson(); j["config"] = config.toJson(); j["lastSeen"]["sec"] = lastSeen.tv_sec; j["lastSeen"]["usec"] = lastSeen.tv_usec; j["connected"] = connected; return j; } std::string id; ///< unique client id Host host; ///< host Snapclient snapclient; ///< Snapclient ClientConfig config; ///< Client config timeval lastSeen; ///< last online bool connected{false}; ///< connected to server? }; /// Group config struct Group : public JsonConfigItem { /// c'tor explicit Group(const ClientInfoPtr& client = nullptr) { if (client) id = client->id; id = generateUUID(); } void fromJson(const json& j) override { name = strutils::trim_copy(jGet(j, "name", "")); id = strutils::trim_copy(jGet(j, "id", "")); streamId = strutils::trim_copy(jGet(j, "stream_id", "")); muted = jGet(j, "muted", false); clients.clear(); if (j.count("clients")) { for (const auto& jClient : j["clients"]) { ClientInfoPtr client = std::make_shared(); client->fromJson(jClient); client->connected = false; addClient(client); } } } json toJson() override { json j; j["name"] = strutils::trim_copy(name); j["id"] = strutils::trim_copy(id); j["stream_id"] = strutils::trim_copy(streamId); j["muted"] = muted; json jClients = json::array(); for (const auto& client : clients) jClients.push_back(client->toJson()); j["clients"] = jClients; return j; } /// Remove client with id @p client_id from the group and @return the removed client ClientInfoPtr removeClient(const std::string& client_id) { for (auto iter = clients.begin(); iter != clients.end(); ++iter) { if ((*iter)->id == client_id) { clients.erase(iter); return (*iter); } } return nullptr; } /// Remove client @p client from the group and @return the removed client ClientInfoPtr removeClient(const ClientInfoPtr& client) { if (!client) return nullptr; return removeClient(client->id); } /// @return client with id @p client_id ClientInfoPtr getClient(const std::string& client_id) { for (const auto& client : clients) { if (client->id == client_id) return client; } return nullptr; } /// Add client @p client void addClient(const ClientInfoPtr& client) { if (!client) return; for (const auto& c : clients) { if (c->id == client->id) return; } clients.push_back(client); /* sort(clients.begin(), clients.end(), [](const ClientInfoPtr a, const ClientInfoPtr b) -> bool { return a.name > b.name; }); */ } /// @return if the group is empty bool empty() const { return clients.empty(); } std::string name; ///< group name std::string id; ///< group id std::string streamId; ///< stream id assigned to this group bool muted{false}; ///< is the group muted? std::vector clients; ///< list of clients in this group }; /// Runtime configuration class Config { public: /// singleton c'tor static Config& instance() { static Config instance_; return instance_; } /// @return client from @p client_id ClientInfoPtr getClientInfo(const std::string& client_id) const; /// Get or create client with id @p client_id and return its group (create a new group for new clients) /// @return the client's group GroupPtr addClientInfo(const std::string& client_id); /// Add client @p client to a group /// @return the client's group: exising or newly created GroupPtr addClientInfo(const ClientInfoPtr& client); /// Remove client @p client from group void remove(const ClientInfoPtr& client); /// Remove group @p group, @p force removal of a non-empty group void remove(const GroupPtr& group, bool force = false); // GroupPtr removeFromGroup(const std::string& groupId, const std::string& clientId); // GroupPtr setGroupForClient(const std::string& groupId, const std::string& clientId); /// @return grouo from client with id @p client_id GroupPtr getGroupFromClient(const std::string& client_id); /// @return group from @p client GroupPtr getGroupFromClient(const ClientInfoPtr& client); /// @return group with id @p group_id GroupPtr getGroup(const std::string& group_id) const; /// @return groups with client as json json getGroups() const; /// @return complete server status, including @p streams json getServerStatus(const json& streams) const; /// Save config to file (json format) void save(); /// Set directory and user/group of persistent "server.json" void init(const std::string& root_directory = "", const std::string& user = "", const std::string& group = ""); /// List of groups std::vector groups; /// to protect members std::mutex& getMutex(); private: /// c'tor Config() = default; /// d'tor ~Config(); mutable std::recursive_mutex mutex_; ///< to protect members std::mutex client_mutex_; ///< returned by "getMutex()", TODO: check this std::string filename_; ///< filename to persist the config };