| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| #include <iostream> |
| #include <string> |
| #include <vector> |
| #include <cstring> |
| #include <cstdlib> |
| #include <future> |
| #include <memory> |
| #include <sstream> |
| #include <iomanip> |
| #include <stdexcept> |
|
|
| #include "bex_engine.h" |
|
|
| |
|
|
| |
| |
| extern "C" void on_bex_result(void* user_data, uint64_t req_id, |
| bool success, const uint8_t* payload, size_t len) { |
| auto* promise = static_cast<std::promise<std::string>*>(user_data); |
|
|
| if (success) { |
| std::string result(reinterpret_cast<const char*>(payload), len); |
| promise->set_value(std::move(result)); |
| } else { |
| std::string error_msg(reinterpret_cast<const char*>(payload), len); |
| promise->set_exception(std::make_exception_ptr(std::runtime_error(error_msg))); |
| } |
| } |
|
|
| |
| |
| std::string await_request(BexEngine* engine, uint64_t request_id, |
| std::promise<std::string>& promise, uint32_t timeout_ms = 30000) { |
| auto future = promise.get_future(); |
| auto status = future.wait_for(std::chrono::milliseconds(timeout_ms)); |
| if (status == std::future_status::timeout) { |
| bex_cancel_request(engine, request_id); |
| throw std::runtime_error("Request timed out after " + std::to_string(timeout_ms) + "ms"); |
| } |
| return future.get(); |
| } |
|
|
| |
|
|
| void print_json(const std::string& json_str) { |
| bool already_pretty = json_str.find('\n') != std::string::npos; |
| if (already_pretty) { |
| std::cout << json_str << std::endl; |
| return; |
| } |
|
|
| int indent = 0; |
| bool in_string = false; |
| bool escape = false; |
| for (size_t i = 0; i < json_str.size(); i++) { |
| char c = json_str[i]; |
|
|
| if (escape) { |
| std::cout << c; |
| escape = false; |
| continue; |
| } |
|
|
| if (c == '\\' && in_string) { |
| std::cout << c; |
| escape = true; |
| continue; |
| } |
|
|
| if (c == '"') { |
| in_string = !in_string; |
| std::cout << c; |
| continue; |
| } |
|
|
| if (in_string) { |
| std::cout << c; |
| continue; |
| } |
|
|
| switch (c) { |
| case '{': |
| case '[': |
| std::cout << c << "\n"; |
| indent += 2; |
| std::cout << std::string(indent, ' '); |
| break; |
| case '}': |
| case ']': |
| std::cout << "\n"; |
| indent -= 2; |
| std::cout << std::string(indent, ' ') << c; |
| break; |
| case ',': |
| std::cout << c << "\n" << std::string(indent, ' '); |
| break; |
| case ':': |
| std::cout << c << " "; |
| break; |
| case ' ': |
| case '\n': |
| case '\r': |
| case '\t': |
| break; |
| default: |
| std::cout << c; |
| } |
| } |
| std::cout << std::endl; |
| } |
|
|
| |
| std::string capabilities_str(uint32_t caps) { |
| std::string result; |
| if (caps & (1 << 0)) result += "HOME "; |
| if (caps & (1 << 1)) result += "CATEGORY "; |
| if (caps & (1 << 2)) result += "SEARCH "; |
| if (caps & (1 << 3)) result += "INFO "; |
| if (caps & (1 << 4)) result += "SERVERS "; |
| if (caps & (1 << 5)) result += "STREAM "; |
| if (caps & (1 << 6)) result += "SUBTITLES "; |
| if (caps & (1 << 7)) result += "ARTICLES "; |
| if (!result.empty()) result.pop_back(); |
| return result; |
| } |
|
|
| |
| struct PluginListGuard { |
| BexPluginInfoList list; |
| explicit PluginListGuard(BexPluginInfoList l) : list(l) {} |
| ~PluginListGuard() { bex_plugin_info_list_free(list); } |
| BexPluginInfo* begin() { return list.items; } |
| BexPluginInfo* end() { return list.items + list.count; } |
| size_t size() const { return list.count; } |
| }; |
|
|
| |
| struct CStrGuard { |
| char* ptr; |
| explicit CStrGuard(char* p) : ptr(p) {} |
| ~CStrGuard() { if (ptr) bex_string_free(ptr); } |
| std::string str() const { return ptr ? std::string(ptr) : std::string(); } |
| }; |
|
|
| |
|
|
| void print_usage(const char* prog) { |
| std::cerr |
| << "BEX C++ CLI v4.0 β WASM Plugin Engine (Pure C ABI + Callbacks)\n" |
| << "\n" |
| << "Usage:\n" |
| << " " << prog << " install <path> Install a .bex plugin package\n" |
| << " " << prog << " uninstall <id> Uninstall a plugin by ID\n" |
| << " " << prog << " list List installed plugins\n" |
| << " " << prog << " info-plugin <id> Show detailed plugin information\n" |
| << " " << prog << " enable <id> Enable a plugin\n" |
| << " " << prog << " disable <id> Disable a plugin\n" |
| << "\n" |
| << " API Key / Secret Management:\n" |
| << " " << prog << " set-key <plugin_id> <key> <val> Set an API key for a plugin\n" |
| << " " << prog << " get-key <plugin_id> <key> Get an API key value\n" |
| << " " << prog << " delete-key <plugin_id> <key> Delete an API key\n" |
| << " " << prog << " list-keys <plugin_id> List all keys for a plugin\n" |
| << "\n" |
| << " Media Browsing (async with callbacks):\n" |
| << " " << prog << " home <plugin_id> Get home sections\n" |
| << " " << prog << " search <plugin_id> <query> Search media\n" |
| << " " << prog << " info <plugin_id> <id> Get media info\n" |
| << " " << prog << " servers <plugin_id> <id> Get servers (id is self-describing)\n" |
| << " " << prog << " stream <plugin_id> <json> Resolve stream from server JSON\n" |
| << "\n" |
| << " Debug:\n" |
| << " " << prog << " stats Show engine stats\n" |
| << "\n" |
| << "Design: IDs are self-describing. The plugin knows how to parse its own IDs.\n" |
| << "Example: bexcli servers com.gogoanime 'one-piece$ep=1$sub=1$dub=0'\n" |
| << std::endl; |
| } |
|
|
| |
|
|
| int main(int argc, char* argv[]) { |
| if (argc < 2) { |
| print_usage(argv[0]); |
| return 1; |
| } |
|
|
| std::string data_dir = std::string(getenv("HOME") ? getenv("HOME") : ".") + "/.bex-data"; |
| if (getenv("BEX_DATA_DIR")) { |
| data_dir = getenv("BEX_DATA_DIR"); |
| } |
|
|
| std::string cmd = argv[1]; |
|
|
| |
| BexEngine* engine = bex_engine_new(data_dir.c_str()); |
| if (!engine) { |
| std::cerr << "Error: Failed to create BexEngine" << std::endl; |
| return 1; |
| } |
|
|
| try { |
| |
|
|
| if (cmd == "install" && argc >= 3) { |
| int rc = bex_engine_install(engine, argv[2]); |
| if (rc != 0) { |
| CStrGuard err(bex_engine_last_error(engine)); |
| throw std::runtime_error("Install failed: " + err.str()); |
| } |
| std::cout << "Plugin installed from: " << argv[2] << std::endl; |
| } |
| else if (cmd == "uninstall" && argc >= 3) { |
| int rc = bex_engine_uninstall(engine, argv[2]); |
| if (rc != 0) { |
| CStrGuard err(bex_engine_last_error(engine)); |
| throw std::runtime_error("Uninstall failed: " + err.str()); |
| } |
| std::cout << "Plugin uninstalled: " << argv[2] << std::endl; |
| } |
| else if (cmd == "list") { |
| PluginListGuard list(bex_engine_list_plugins(engine)); |
| if (list.size() == 0) { |
| std::cout << "No plugins installed." << std::endl; |
| } else { |
| std::cout << std::left |
| << std::setw(40) << "ID" |
| << std::setw(20) << "NAME" |
| << std::setw(10) << "VERSION" |
| << std::setw(10) << "STATUS" |
| << "CAPABILITIES" << std::endl; |
| std::cout << std::string(100, '-') << std::endl; |
| for (size_t i = 0; i < list.size(); i++) { |
| auto& p = list.list.items[i]; |
| std::cout << std::left |
| << std::setw(40) << (p.id ? p.id : "") |
| << std::setw(20) << (p.name ? p.name : "") |
| << std::setw(10) << (p.version ? p.version : "") |
| << std::setw(10) << (p.enabled ? "enabled" : "disabled") |
| << capabilities_str(p.capabilities) << std::endl; |
| } |
| } |
| } |
| else if (cmd == "info-plugin" && argc >= 3) { |
| BexPluginInfo info; |
| int rc = bex_engine_plugin_info(engine, argv[2], &info); |
| if (rc != 0) { |
| CStrGuard err(bex_engine_last_error(engine)); |
| throw std::runtime_error("Plugin info failed: " + err.str()); |
| } |
|
|
| std::cout << "ID: " << (info.id ? info.id : "") << std::endl; |
| std::cout << "Name: " << (info.name ? info.name : "") << std::endl; |
| std::cout << "Version: " << (info.version ? info.version : "") << std::endl; |
| std::cout << "Enabled: " << (info.enabled ? "yes" : "no") << std::endl; |
| std::cout << "Capabilities: " << capabilities_str(info.capabilities) << std::endl; |
|
|
| |
| CStrGuard keys(bex_engine_secret_keys(engine, argv[2])); |
| if (keys.ptr && strlen(keys.ptr) > 0) { |
| std::cout << "API Keys: " << keys.str() << std::endl; |
| } else { |
| std::cout << "API Keys: (none)" << std::endl; |
| } |
|
|
| bex_plugin_info_free(info); |
| } |
| else if (cmd == "enable" && argc >= 3) { |
| int rc = bex_engine_enable(engine, argv[2]); |
| if (rc != 0) { |
| CStrGuard err(bex_engine_last_error(engine)); |
| throw std::runtime_error("Enable failed: " + err.str()); |
| } |
| std::cout << "Enabled: " << argv[2] << std::endl; |
| } |
| else if (cmd == "disable" && argc >= 3) { |
| int rc = bex_engine_disable(engine, argv[2]); |
| if (rc != 0) { |
| CStrGuard err(bex_engine_last_error(engine)); |
| throw std::runtime_error("Disable failed: " + err.str()); |
| } |
| std::cout << "Disabled: " << argv[2] << std::endl; |
| } |
|
|
| |
|
|
| else if (cmd == "set-key" && argc >= 5) { |
| int rc = bex_engine_secret_set(engine, argv[2], argv[3], argv[4]); |
| if (rc != 0) { |
| CStrGuard err(bex_engine_last_error(engine)); |
| throw std::runtime_error("Set key failed: " + err.str()); |
| } |
| std::cout << "Key '" << argv[3] << "' set for plugin '" << argv[2] << "'" << std::endl; |
| } |
| else if (cmd == "get-key" && argc >= 4) { |
| char buf[4096]; |
| size_t buf_len = sizeof(buf); |
| int rc = bex_engine_secret_get(engine, argv[2], argv[3], buf, &buf_len); |
| if (rc == 0) { |
| std::cout << buf << std::endl; |
| } else { |
| std::cout << "Key '" << argv[3] << "' not found for plugin '" << argv[2] << "'" << std::endl; |
| } |
| } |
| else if (cmd == "delete-key" && argc >= 4) { |
| int rc = bex_engine_secret_delete(engine, argv[2], argv[3]); |
| if (rc == 0) { |
| std::cout << "Key '" << argv[3] << "' deleted from plugin '" << argv[2] << "'" << std::endl; |
| } else if (rc == 1) { |
| std::cout << "Key '" << argv[3] << "' not found for plugin '" << argv[2] << "'" << std::endl; |
| } else { |
| CStrGuard err(bex_engine_last_error(engine)); |
| throw std::runtime_error("Delete key failed: " + err.str()); |
| } |
| } |
| else if (cmd == "list-keys" && argc >= 3) { |
| CStrGuard keys(bex_engine_secret_keys(engine, argv[2])); |
| if (keys.ptr && strlen(keys.ptr) > 0) { |
| std::cout << "Keys for plugin '" << argv[2] << "': " << keys.str() << std::endl; |
| } else { |
| std::cout << "No keys found for plugin '" << argv[2] << "'" << std::endl; |
| } |
| } |
|
|
| |
|
|
| else if (cmd == "home" && argc >= 3) { |
| std::promise<std::string> promise; |
| uint64_t req_id = bex_submit_home(engine, argv[2], on_bex_result, &promise); |
| std::string result = await_request(engine, req_id, promise); |
| print_json(result); |
| } |
| else if (cmd == "search" && argc >= 4) { |
| std::promise<std::string> promise; |
| uint64_t req_id = bex_submit_search(engine, argv[2], argv[3], on_bex_result, &promise); |
| std::string result = await_request(engine, req_id, promise); |
| print_json(result); |
| } |
| else if (cmd == "info" && argc >= 4) { |
| std::promise<std::string> promise; |
| uint64_t req_id = bex_submit_info(engine, argv[2], argv[3], on_bex_result, &promise); |
| std::string result = await_request(engine, req_id, promise); |
| print_json(result); |
| } |
| else if (cmd == "servers" && argc >= 4) { |
| |
| |
| std::promise<std::string> promise; |
| uint64_t req_id = bex_submit_servers(engine, argv[2], argv[3], on_bex_result, &promise); |
| std::string result = await_request(engine, req_id, promise); |
| print_json(result); |
| } |
| else if (cmd == "stream" && argc >= 4) { |
| std::promise<std::string> promise; |
| uint64_t req_id = bex_submit_stream(engine, argv[2], argv[3], on_bex_result, &promise); |
| std::string result = await_request(engine, req_id, promise); |
| print_json(result); |
| } |
|
|
| |
|
|
| else if (cmd == "stats") { |
| CStrGuard stats(bex_engine_stats(engine)); |
| if (stats.ptr) { |
| print_json(stats.str()); |
| } else { |
| std::cout << "Unable to get stats" << std::endl; |
| } |
| } |
| else { |
| print_usage(argv[0]); |
| bex_engine_free(engine); |
| return 1; |
| } |
|
|
| bex_engine_free(engine); |
| } |
| catch (const std::exception& e) { |
| std::cerr << "Error: " << e.what() << std::endl; |
| bex_engine_free(engine); |
| return 1; |
| } |
|
|
| return 0; |
| } |
|
|