Ctrl K
ComunidadGuías…

lms_icon.h

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
#pragma once
#include "queue_lms_cli.h"
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <filesystem>
#include <fstream>
#include <functional>
#include <mutex>
#include <optional>
#include <set>
#include <stdexcept>
#include <thread>
namespace bridge {
// JSON validation and semantic recognition of the one-property icon objects
// YeneY writes. Other valid JSON values are owner selections, never ours.
class IconJson {
    const std::string &s;
    size_t at = 0;
    void space() {
        while (at < s.size() && (s[at] == ' ' || s[at] == '\t' || s[at] == '\r' || s[at] == '\n'))
            ++at;
    }
    bool eat(char c) {
        space();
        if (at < s.size() && s[at] == c) {
            ++at;
            return true;
        }
        return false;
    }
    [[noreturn]] void bad() { throw std::runtime_error("malformed icon JSON"); }
    unsigned hex4() {
        unsigned n = 0;
        for (unsigned i = 0; i < 4; ++i) {
            if (at == s.size())
                bad();
            const char c = s[at++];
            int v = c >= '0' && c <= '9'   ? c - '0'
                    : c >= 'a' && c <= 'f' ? c - 'a' + 10
                    : c >= 'A' && c <= 'F' ? c - 'A' + 10
                                           : -1;
            if (v < 0)
                bad();
            n = n * 16 + v;
        }
        return n;
    }
    static void utf8(std::string &out, unsigned n) {
        if (n < 128)
            out += char(n);
        else if (n < 2048) {
            out += char(192 | (n >> 6));
            out += char(128 | (n & 63));
        } else if (n < 65536) {
            out += char(224 | (n >> 12));
            out += char(128 | ((n >> 6) & 63));
            out += char(128 | (n & 63));
        } else {
            out += char(240 | (n >> 18));
            out += char(128 | ((n >> 12) & 63));
            out += char(128 | ((n >> 6) & 63));
            out += char(128 | (n & 63));
        }
    }
    std::string string() {
        if (!eat('"'))
            bad();
        std::string out;
        while (at < s.size()) {
            unsigned char c = s[at++];
            if (c == '"')
                return out;
            if (c < 32)
                bad();
            if (c == '\\') {
                if (at == s.size())
                    bad();
                c = s[at++];
                switch (c) {
                case '"':
                case '\\':
                case '/':
                    out += char(c);
                    break;
                case 'b':
                    out += '\b';
                    break;
                case 'f':
                    out += '\f';
                    break;
                case 'n':
                    out += '\n';
                    break;
                case 'r':
                    out += '\r';
                    break;
                case 't':
                    out += '\t';
                    break;
                case 'u': {
                    unsigned n = hex4();
                    if (n >= 0xd800 && n <= 0xdbff) {
                        if (at + 2 > s.size() || s.substr(at, 2) != "\\u")
                            bad();
                        at += 2;
                        unsigned low = hex4();
                        if (low < 0xdc00 || low > 0xdfff)
                            bad();
                        n = 0x10000 + (n - 0xd800) * 1024 + low - 0xdc00;
                    } else if (n >= 0xdc00 && n <= 0xdfff)
                        bad();
                    utf8(out, n);
                    break;
                }
                default:
                    bad();
                }
            } else if (c < 128)
                out += char(c);
            else {
                unsigned n = 0, count = 0, min = 0;
                if (c >= 194 && c <= 223) {
                    n = c & 31;
                    count = 1;
                    min = 128;
                } else if (c >= 224 && c <= 239) {
                    n = c & 15;
                    count = 2;
                    min = 2048;
                } else if (c >= 240 && c <= 244) {
                    n = c & 7;
                    count = 3;
                    min = 65536;
                } else
                    bad();
                for (unsigned i = 0; i < count; ++i) {
                    if (at == s.size())
                        bad();
                    unsigned b = static_cast<unsigned char>(s[at++]);
                    if ((b & 192) != 128)
                        bad();
                    n = (n << 6) | (b & 63);
                }
                if (n < min || n > 0x10ffff || (n >= 0xd800 && n <= 0xdfff))
                    bad();
                utf8(out, n);
            }
        }
        bad();
    }
    std::optional<std::string> value(unsigned depth) {
        if (depth > 32)
            bad();
        space();
        if (at == s.size())
            bad();
        if (s[at] == '{') {
            ++at;
            std::set<std::string> keys;
            std::optional<std::string> icon;
            if (!eat('}')) {
                do {
                    auto key = string();
                    if (!keys.insert(key).second || !eat(':'))
                        bad();
                    space();
                    bool text = at < s.size() && s[at] == '"';
                    if (text) {
                        auto v = string();
                        if (key == "svg")
                            icon = v;
                    } else
                        value(depth + 1);
                } while (eat(','));
                if (!eat('}'))
                    bad();
            }
            return keys.size() == 1 && keys.count("svg") ? icon : std::nullopt;
        }
        if (s[at] == '[') {
            ++at;
            if (!eat(']')) {
                do {
                    value(depth + 1);
                } while (eat(','));
                if (!eat(']'))
                    bad();
            }
            return {};
        }
        if (s[at] == '"') {
            string();
            return {};
        }
        for (auto word : {"null", "true", "false"}) {
            size_t n = std::char_traits<char>::length(word);
            if (s.compare(at, n, word) == 0) {
                at += n;
                return {};
            }
        }
        if (s[at] == '-')
            ++at;
        if (at == s.size())
            bad();
        if (s[at] == '0')
            ++at;
        else {
            if (s[at] < '1' || s[at] > '9')
                bad();
            while (at < s.size() && s[at] >= '0' && s[at] <= '9')
                ++at;
        }
        if (at < s.size() && s[at] == '.') {
            ++at;
            auto start = at;
            while (at < s.size() && s[at] >= '0' && s[at] <= '9')
                ++at;
            if (at == start)
                bad();
        }
        if (at < s.size() && (s[at] == 'e' || s[at] == 'E')) {
            ++at;
            if (at < s.size() && (s[at] == '+' || s[at] == '-'))
                ++at;
            auto start = at;
            while (at < s.size() && s[at] >= '0' && s[at] <= '9')
                ++at;
            if (at == start)
                bad();
        }
        return {};
    }

public:
    explicit IconJson(const std::string &text) : s(text) {}
    std::optional<std::string> parse() {
        auto v = value(0);
        space();
        if (at != s.size())
            bad();
        return v;
    }
};
inline std::string iconEncode(const std::string &s) {
    static const char hex[] = "0123456789ABCDEF";
    std::string out;
    for (unsigned char c : s) {
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' ||
            c == '_' || c == '.' || c == '~')
            out += char(c);
        else {
            out += '%';
            out += hex[c >> 4];
            out += hex[c & 15];
        }
    }
    return out;
}
inline std::vector<std::string> iconTokens(const std::string &raw) {
    std::istringstream input(raw);
    std::vector<std::string> values;
    std::string token;
    while (input >> token)
        values.push_back(queue_audio::decodeLms(token));
    return values;
}
class LmsIcon {
public:
    using Log = std::function<void(const std::string &)>;

private:
    std::string mac, name, directory;
    Log log;
    unsigned port;
    std::mutex mutex;
    std::condition_variable changed;
    std::deque<std::string> jobs;
    std::atomic_bool cancel{false};
    bool stopping = false, enabled = true;
    std::thread worker;
    static constexpr const char *key = "plugin.material-skin:icon";
    void save(const std::filesystem::path &path, const std::string &json) {
        auto temporary = path.string() + ".XXXXXX";
        std::vector<char> bytes(temporary.begin(), temporary.end());
        bytes.push_back(0);
        int fd = mkstemp(bytes.data());
        if (fd < 0)
            throw std::runtime_error("state unavailable");
        bool ok = ::write(fd, json.data(), json.size()) == ssize_t(json.size()) && fsync(fd) == 0;
        if (close(fd) != 0)
            ok = false;
        if (ok)
            ok = ::rename(bytes.data(), path.c_str()) == 0;
        if (!ok) {
            unlink(bytes.data());
            throw std::runtime_error("state write failed");
        }
    }
    std::string apply(const std::string &host) {
        if (directory.empty() || !std::filesystem::is_directory(directory))
            return "state directory unavailable";
        std::string reply;
        auto call = [&](const std::string &cmd, bool global = false, bool empty = false,
                        unsigned timeout = 3000) {
            return queue_audio::controlLms(host, mac, cmd, port, &reply, timeout, &cancel, global, empty);
        };
        if (!call("players 0 1000", true))
            return "players query failed";
        auto players = iconTokens(reply);
        bool visible = false;
        for (auto token : players) {
            std::transform(token.begin(), token.end(), token.begin(),
                           [](unsigned char c) { return char(std::tolower(c)); });
            if (token == "playerid:" + mac)
                visible = true;
        }
        if (!visible)
            return "player not visible";
        const auto path = std::filesystem::path(directory) / ("material-icon-" + mac + ".json");
        std::optional<std::string> previous;
        if (std::filesystem::exists(path)) {
            if (std::filesystem::is_symlink(path))
                return "state symlink refused";
            std::ifstream file(path);
            std::string text((std::istreambuf_iterator<char>(file)), {});
            previous = IconJson(text).parse();
            if (!previous)
                return "invalid state";
        }
        if (!call(std::string("playerpref ") + key + " ?", false, true))
            return "preference query failed";
        auto tokens = iconTokens(reply);
        if (tokens.size() < 3 || tokens.size() > 4)
            return "malformed preference reply";
        auto current = tokens.size() == 4 ? tokens.back() : std::string{};
        std::optional<std::string> selected;
        if (!current.empty())
            selected = IconJson(current).parse();
        if (selected && *selected == name)
            return "already configured";
        if (!current.empty() && (!selected || !previous || *selected != *previous))
            return "owner selection preserved";
        auto json = "{\"svg\":\"" + name + "\"}";
        if (!call(std::string("playerpref ") + key + " " + iconEncode(json), false, false, 15000))
            return "write unconfirmed";
        save(path, json);
        return "set svg=" + name;
    }
    void run() {
        while (true) {
            std::string host;
            {
                std::unique_lock<std::mutex> lock(mutex);
                changed.wait(lock, [&] { return stopping || !jobs.empty(); });
                if (stopping)
                    return;
                host = std::move(jobs.front());
                jobs.pop_front();
                cancel = false;
            }
            std::string outcome;
            try {
                outcome = apply(host);
            } catch (const std::exception &) {
                outcome = "invalid response or state";
            }
            log("LMS icon: " + outcome);
        }
    }

public:
    LmsIcon(const uint8_t *address, Log logger, unsigned cliPort = 9090)
        : log(std::move(logger)), port(cliPort) {
        char text[18];
        std::snprintf(text, sizeof(text), "%02x:%02x:%02x:%02x:%02x:%02x", address[0], address[1], address[2],
                      address[3], address[4], address[5]);
        mac = text;
        const char *setting = std::getenv("YENEY_LMS_ICON");
        name = setting ? setting : "sonos";
        enabled = !name.empty();
        if (name.size() > 64 ||
            name.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-") !=
                std::string::npos) {
            enabled = false;
            log("LMS icon: warning invalid YENEY_LMS_ICON; automatic handling disabled");
        }
        if (const char *state = std::getenv("STATE_DIRECTORY"))
            directory = state;
        if (!directory.empty() &&
            (!std::filesystem::path(directory).is_absolute() || directory.find(':') != std::string::npos))
            directory.clear();
        if (enabled)
            worker = std::thread([this] { run(); });
    }
    ~LmsIcon() { shutdown(); }
    void shutdown() {
        {
            std::lock_guard<std::mutex> lock(mutex);
            stopping = true;
            cancel = true;
        }
        changed.notify_all();
        if (worker.joinable())
            worker.join();
    }
    void connected(const std::string &host) {
        if (!enabled) {
            log("LMS icon: automatic handling disabled");
            return;
        }
        std::lock_guard<std::mutex> lock(mutex);
        cancel = true;
        while (!jobs.empty()) {
            jobs.pop_front();
            log("LMS icon: connection superseded");
        }
        jobs.push_back(host);
        changed.notify_all();
    }
};
} // namespace bridge