/** @file src/confighttp_steam.cpp */ #include "config.h" #include "config_steam.h" #include "confighttp.h" #include "file_handler.h" #include "platform/common.h" #include "steam_artwork.h" #include "steam_integration.h" #include "steam_sync_policy.h" #include #include #include #include #include #include namespace confighttp { using resp_https_t = std::shared_ptr::Response>; using req_https_t = std::shared_ptr::Request>; bool authenticate(resp_https_t response, req_https_t request); bool check_content_type(resp_https_t response, req_https_t request, const std::string_view &contentType); void send_response(resp_https_t response, const nlohmann::json &output_tree); void bad_request(resp_https_t response, req_https_t request, const std::string &error_message = "Bad Request"); namespace { nlohmann::json game_json(const platf::steam::game_t &game) { nlohmann::json out; out["appid"] = game.app_id; out["steam_id"] = std::to_string(game.app_id); out["stable_id"] = game.stable_id; out["name"] = game.name; out["install_dir"] = game.install_dir.generic_string(); out["library_path"] = game.library_path.generic_string(); out["icon_path"] = game.icon_path.generic_string(); out["header_path"] = game.header_path.generic_string(); out["portrait_path"] = game.portrait_path.generic_string(); out["boxart_path"] = game.boxart_path.generic_string(); out["artwork_path"] = game.artwork_path.generic_string(); out["artwork_client_path"] = game.artwork_client_path.generic_string(); out["artwork_format"] = game.artwork_format; out["artwork_client_compatible"] = !game.artwork_client_path.empty(); out["app_type"] = game.app_type; out["installed"] = game.installed; out["last_played"] = game.last_played; out["playtime_minutes"] = game.playtime_minutes; out["importable"] = platf::steam::sync::policy::is_importable(game, config::steam.include_tools); out["launch_uri"] = platf::steam::launch_uri(game.app_id); return out; } } // namespace void getSteamStatus(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } try { const auto provider_available = platf::steam::available(); auto found = platf::steam::discover(); const auto all_importable = platf::steam::sync::policy::filter_games(found, {}, config::steam.include_tools); const auto selected = platf::steam::sync::policy::select_games( found, config::steam.sync_all_installed, config::steam.recent_games, config::steam.recent_max_age_days, config::steam.exclude_games_meta, config::steam.include_tools ); const auto selected_importable = platf::steam::sync::policy::filter_games(selected, {}, config::steam.include_tools); const auto filtered = platf::steam::sync::policy::filter_games(selected, config::steam.exclude_games_meta, config::steam.include_tools); const auto all_filtered = platf::steam::sync::policy::filter_games(found, config::steam.exclude_games_meta, config::steam.include_tools); nlohmann::json exclusions = nlohmann::json::array(); for (const auto &entry : config::steam.exclude_games_meta) { exclusions.push_back({{"id", entry.id}, {"name", entry.name}}); } nlohmann::json out {{"status", true}, {"provider", "steam"}, {"enabled", config::steam.enabled}, {"forced", false}, {"available", provider_available}, {"game_count", found.size()}, {"importable_game_count", filtered.size()}, {"tool_game_count", found.size() - all_importable.size()}, {"excluded_game_count", all_importable.size() - all_filtered.size()}, {"selected_game_count", selected_importable.size()}, {"exclude_games", std::move(exclusions)}, {"auto_sync", config::steam.auto_sync}, {"sync_all_installed", config::steam.sync_all_installed}, {"recent_games", config::steam.recent_games}, {"recent_max_age_days", config::steam.recent_max_age_days}, {"autosync_remove_uninstalled", config::steam.autosync_remove_uninstalled}, {"include_tools", config::steam.include_tools}}; #if defined(__linux__) out["forced"] = true; out["playnite_available"] = false; #else out["playnite_available"] = true; #endif send_response(response, out); } catch (const std::exception &error) { bad_request(response, request, error.what()); } } void getSteamGames(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } try { nlohmann::json out {{"status", true}, {"enabled", config::steam.enabled}, {"games", nlohmann::json::array()}}; auto games = platf::steam::discover_catalog(); // Prepare only the selected game's cover. Browsing a large library // must not download or convert artwork for every search result. const auto query = request->parse_query_string(); if (const auto selected = query.find("appid"); selected != query.end()) { std::erase_if(games, [&](const auto &game) { return std::to_string(game.app_id) != selected->second; }); platf::steam::artwork::prepare(games, platf::appdata()); } for (const auto &game : games) { auto item = game_json(game); item["excluded"] = std::find_if(config::steam.exclude_games_meta.begin(), config::steam.exclude_games_meta.end(), [&game](const auto &entry) { return (!entry.id.empty() && entry.id == std::to_string(game.app_id)) || (entry.id.empty() && !entry.name.empty() && entry.name == game.name); }) != config::steam.exclude_games_meta.end(); item["filtered"] = !platf::steam::sync::policy::is_importable(game, config::steam.include_tools); out["games"].push_back(std::move(item)); } send_response(response, out); } catch (const std::exception &error) { bad_request(response, request, error.what()); } } void postSteamForceSync(resp_https_t response, req_https_t request) { if (!check_content_type(response, request, "application/json")) { return; } if (!authenticate(response, request)) { return; } if (!config::steam.enabled) { bad_request(response, request, "Steam integration is disabled"); return; } try { if (!platf::steam::available()) { bad_request(response, request, "Steam installation was not found"); return; } auto catalog = platf::steam::discover_catalog(); auto found = platf::steam::sync::policy::select_games( catalog, config::steam.sync_all_installed, config::steam.recent_games, config::steam.recent_max_age_days, config::steam.exclude_games_meta, config::steam.include_tools ); platf::steam::artwork::prepare(found, platf::appdata()); std::lock_guard apps_lock {apps_file_mutex()}; auto root = nlohmann::json::parse(file_handler::read_file(config::stream.file_apps.c_str())); const bool changed = platf::steam::sync::policy::reconcile(root, found, !config::steam.sync_all_installed || config::steam.autosync_remove_uninstalled, config::steam.exclude_games_meta, config::steam.include_tools, config::steam.sync_all_installed ? "installed" : "recent"); if (changed && !refresh_client_apps_cache(root)) { bad_request(response, request, "Unable to save applications"); return; } const auto filtered = platf::steam::sync::policy::filter_games(found, config::steam.exclude_games_meta, config::steam.include_tools); send_response(response, nlohmann::json {{"status", true}, {"changed", changed}, {"game_count", found.size()}, {"importable_game_count", filtered.size()}}); } catch (const std::exception &error) { bad_request(response, request, error.what()); } } void postSteamLaunch(resp_https_t response, req_https_t request) { if (!check_content_type(response, request, "application/json")) { return; } if (!authenticate(response, request)) { return; } if (!config::steam.enabled) { bad_request(response, request, "Steam integration is disabled"); return; } try { std::stringstream stream; stream << request->content.rdbuf(); const auto input = nlohmann::json::parse(stream.str()); std::uint64_t app_id = 0; const auto id = input.contains("appid") ? input["appid"] : input.value("steam_id", nlohmann::json {}); if (id.is_number_unsigned() || id.is_number_integer()) { app_id = id.get(); } else if (id.is_string()) { const auto text = id.get(); if (text.empty() || !std::all_of(text.begin(), text.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) { bad_request(response, request, "Steam app ID must be numeric"); return; } app_id = std::stoull(text); } if (app_id == 0 || app_id > UINT32_MAX) { bad_request(response, request, "Invalid Steam app ID or Steam launch failed"); return; } const auto requested_id = static_cast(app_id); const auto launchable = platf::steam::sync::policy::filter_games( platf::steam::discover(), config::steam.exclude_games_meta, config::steam.include_tools ); const auto installed = std::find_if(launchable.begin(), launchable.end(), [requested_id](const auto &game) { return game.app_id == requested_id; }); if (installed == launchable.end() || !platf::steam::launch(requested_id)) { bad_request(response, request, "Steam app ID is not installed, is excluded, or Steam launch failed"); return; } send_response(response, nlohmann::json {{"status", true}, {"launch_uri", platf::steam::launch_uri(static_cast(app_id))}}); } catch (const std::exception &error) { bad_request(response, request, error.what()); } } } // namespace confighttp