diff --git a/api_output.py b/api_output.py index abc2fe4..d17724b 100644 --- a/api_output.py +++ b/api_output.py @@ -4,7 +4,7 @@ import io from functools import partial import xml.etree.ElementTree as ET -from urllib.parse import urlparse +from urllib.parse import urlparse, urlencode import threading from sc_logging import logger @@ -12,7 +12,8 @@ from storage import fetch_data, subscribe_to_data out_api_url = fetch_data("scoresight.json", "out_api_url", None) -out_api_encoding = fetch_data("scoresight.json", "out_api_encoding", "JSON") +out_api_encoding = fetch_data("scoresight.json", "out_api_encoding", "JSON (Full)") +out_api_method = fetch_data("scoresight.json", "out_api_method", "POST") def is_valid_url_urllib(url): @@ -33,13 +34,21 @@ def setup_out_api_encoding(encoding): out_api_encoding = encoding +def setup_out_api_method(method): + global out_api_method + out_api_method = method + + subscribe_to_data("scoresight.json", "out_api_url", setup_out_api_url) subscribe_to_data("scoresight.json", "out_api_encoding", setup_out_api_encoding) +subscribe_to_data("scoresight.json", "out_api_method", setup_out_api_method) def update_out_api(data: list[TextDetectionTargetWithResult]): - if out_api_url is None or out_api_encoding is None: - logger.error(f"Output API not set up: {out_api_url}, {out_api_encoding}") + if out_api_url is None or out_api_encoding is None or out_api_method is None: + logger.error( + f"Output API not set up: {out_api_url}, {out_api_encoding}, {out_api_method}" + ) return # validate the URL @@ -51,14 +60,20 @@ def update_out_api(data: list[TextDetectionTargetWithResult]): def send_data(): try: - if out_api_encoding == "JSON": - response = send_json(data) - elif out_api_encoding == "XML": - response = send_xml(data) - elif out_api_encoding == "CSV": - response = send_csv(data) + if out_api_method == "GET": + response = send_get(data) else: - logger.error("Invalid encoding: %s", out_api_encoding) + if out_api_encoding.startswith("JSON"): + response = send_json(data, out_api_encoding) + elif out_api_encoding == "XML": + response = send_xml(data) + elif out_api_encoding == "CSV": + response = send_csv(data) + else: + logger.error("Invalid encoding: %s", out_api_encoding) + return + + if response is None: return if response.status_code != 200: @@ -72,13 +87,41 @@ def send_data(): thread.start() -def send_json(data: list[TextDetectionTargetWithResult]): +def send_get(data: list[TextDetectionTargetWithResult]): + out_api_url_copy = out_api_url + # add the data to the URL as query parameters + # check if the URL already has query parameters + if "?" in out_api_url_copy: + out_api_url_copy += "&" + else: + out_api_url_copy += "?" + out_api_url_copy += urlencode({result.name: result.result for result in data}) + logger.debug(f"GET URL: {out_api_url_copy}") + response = requests.get(out_api_url_copy) + return response + + +def send_json(data: list[TextDetectionTargetWithResult], encoding: str): headers = {"Content-Type": "application/json"} - response = requests.post( - out_api_url, - headers=headers, - data=json.dumps([result.to_dict() for result in data]), - ) + if encoding == "JSON (Full)": + json_data_dump = json.dumps([result.to_dict() for result in data]) + elif encoding == "JSON (Simple key-value)": + json_data_dump = json.dumps({result.name: result.result for result in data}) + if out_api_method == "POST": + response = requests.post( + out_api_url, + headers=headers, + data=json_data_dump, + ) + elif out_api_method == "PUT": + response = requests.put( + out_api_url, + headers=headers, + data=json_data_dump, + ) + else: + logger.error(f"Invalid method: {out_api_method}") + return None return response @@ -95,7 +138,13 @@ def send_xml(data: list[TextDetectionTargetWithResult]): resultEl.set("width", str(targetWithResult.width())) resultEl.set("height", str(targetWithResult.height())) xml_data = ET.tostring(root, encoding="utf-8") - response = requests.post(out_api_url, headers=headers, data=xml_data) + if out_api_method == "POST": + response = requests.post(out_api_url, headers=headers, data=xml_data) + elif out_api_method == "PUT": + response = requests.put(out_api_url, headers=headers, data=xml_data) + else: + logger.error(f"Invalid method: {out_api_method}") + return None return response @@ -116,5 +165,11 @@ def send_csv(data: list[TextDetectionTargetWithResult]): result.height(), ] ) - response = requests.post(out_api_url, headers=headers, data=output.getvalue()) + if out_api_method == "POST": + response = requests.post(out_api_url, headers=headers, data=output.getvalue()) + elif out_api_method == "PUT": + response = requests.put(out_api_url, headers=headers, data=output.getvalue()) + else: + logger.error(f"Invalid method: {out_api_method}") + return None return response diff --git a/docs/image-29.png b/docs/image-29.png new file mode 100644 index 0000000..79fbe06 Binary files /dev/null and b/docs/image-29.png differ diff --git a/docs/image-30.png b/docs/image-30.png new file mode 100644 index 0000000..3296c81 Binary files /dev/null and b/docs/image-30.png differ diff --git a/docs/out_api.md b/docs/out_api.md index b6392dd..c642c1c 100644 --- a/docs/out_api.md +++ b/docs/out_api.md @@ -1,4 +1,4 @@ -# ScoreSight Outboud API Integration Tutorial +# ScoreSight Outbound API Integration Tutorial ScoreSight now offers the ability to send OCR-extracted scoreboard data to external APIs. This tutorial will guide you through setting up the feature and provide a simple Python script to receive the data. @@ -6,13 +6,15 @@ ScoreSight now offers the ability to send OCR-extracted scoreboard data to exter 1. Open ScoreSight and navigate to the "API" tab in the bottom left corner. -![alt text](image-21.png) +![alt text](image-29.png) 2. Check the box labeled "Send out API requests to external services". 3. Enter the URL where you want to send the data in the provided box. -4. Select the encoding format for the data: JSON, XML, or CSV. +4. Select the encoding format for the data: JSON (Full), Json (Simple key-value) XML, or CSV. -![alt text](image-22.png) +![alt text](image-30.png) + +5. Choose the HTTP method for the API request: POST, PUT, or GET. ## Troubleshooting @@ -35,13 +37,17 @@ If you encounter issues with the API integration, follow these steps to troubles 3. Encoding Format: - Verify that the encoding format selected in ScoreSight (JSON, XML, or CSV) matches the format your receiving script or API expects. -4. Server Availability and Authentication: +4. HTTP Method: + - Ensure that the selected HTTP method (POST, PUT, or GET) is supported by your receiving API. + - Verify that your API is configured to handle the chosen method correctly. + +5. Server Availability and Authentication: - If using the provided Python script, make sure it's running before attempting to send data from ScoreSight. - For external APIs, check if the service is up and accessible. - If your external API requires an API key or authentication, ensure these details are correctly included in the URL or headers. - If testing locally, check that your firewall isn't blocking the connection. -5. Test with a Simple Server: +6. Test with a Simple Server: - Use the provided Python script below as a test server to isolate whether the issue is with ScoreSight or the receiving end. If problems persist after trying these steps, consider reaching out to ScoreSight support for further assistance. @@ -56,16 +62,30 @@ import json class RequestHandler(BaseHTTPRequestHandler): def do_POST(self): - content_length = int(self.headers['Content-Length']) - post_data = self.rfile.read(content_length) - - print("Received data:") - try: - # Assuming JSON format, adjust if using XML or CSV - data = json.loads(post_data.decode('utf-8')) - print(json.dumps(data, indent=2)) - except json.JSONDecodeError: - print(post_data.decode('utf-8')) + self.handle_request() + + def do_PUT(self): + self.handle_request() + + def do_GET(self): + self.handle_request() + + def handle_request(self): + if self.command in ['POST', 'PUT']: + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + + print(f"Received {self.command} data:") + try: + # Assuming JSON format, adjust if using XML or CSV + data = json.loads(post_data.decode('utf-8')) + print(json.dumps(data, indent=2)) + except json.JSONDecodeError: + print(post_data.decode('utf-8')) + elif self.command == 'GET': + print("Received GET request") + print(f"Path: {self.path}") + print(f"Headers: {self.headers}") self.send_response(200) self.end_headers() @@ -100,7 +120,7 @@ You may see in the console an output similar to: ``` 127.0.0.1 - - [nn/nn/nnnn 10:57:03] "POST / HTTP/1.1" 200 - -Received data: Name,Text,State,X,Y,Width,Height +Received POST data: Name,Text,State,X,Y,Width,Height Time,0:52,SameNoChange,843.656319861826,663.0215827338131,359.13669064748206,207.19424460431662 Home Score,35,SameNoChange,525.969716884406,638.4370727628325,214.62426933453253,175.27648662320166 ``` diff --git a/mainwindow.py b/mainwindow.py index 89092bc..a0691db 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -227,6 +227,14 @@ def __init__(self, translator: QTranslator, parent: QObject): fetch_data("scoresight.json", "out_api_encoding", "JSON") ) ) + self.ui.comboBox_outApiMethod.currentTextChanged.connect( + partial(self.globalSettingsChanged, "out_api_method") + ) + self.ui.comboBox_outApiMethod.setCurrentIndex( + self.ui.comboBox_outApiMethod.findText( + fetch_data("scoresight.json", "out_api_method", "POST") + ) + ) self.obs_websocket_client = None diff --git a/mainwindow.ui b/mainwindow.ui index 647e46c..63c3592 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -1457,7 +1457,12 @@ - JSON + JSON (Full) + + + + + JSON (Simple key-value) @@ -1511,16 +1516,28 @@ - - - false - - - Not implemented yet. - - - Websocket + + + + 50 + 16777215 + + + + POST + + + + + PUT + + + + + GET + + diff --git a/sc_logging.py b/sc_logging.py index 09930c0..532267b 100644 --- a/sc_logging.py +++ b/sc_logging.py @@ -4,59 +4,72 @@ from datetime import datetime from dotenv import load_dotenv -# Load the environment variables from the .env file -load_dotenv(os.path.abspath(os.path.join(os.path.dirname(__file__), ".env"))) - -# Create a logger -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - -# get the user data directory -data_dir = user_log_dir("scoresight") -if not os.path.exists(data_dir): - os.makedirs(data_dir) - -current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - -# basic config - send all logs to a file -logging.basicConfig( - filename=os.path.join(data_dir, f"scoresight_std_{current_time}.log"), - level=logging.INFO, -) - -# prepend the user data directory -log_file_path = os.path.join(data_dir, f"scoresight_{current_time}.log") - -# Create a file handler -file_handler = logging.FileHandler(log_file_path) -file_handler.setLevel(logging.DEBUG) - -# Create a formatter -formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(module)s - %(message)s") -file_handler.setFormatter(formatter) - -# Add the file handler to the logger -logger.addHandler(file_handler) - -# if the .env file has a debug flag, set the logger to output to console -if os.getenv("SCORESIGHT_DEBUG"): - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) - console_handler.setFormatter(formatter) - logger.addHandler(console_handler) - logger.debug("Debug mode enabled") - -# check to see if there are more log files, and only keep the most recent 10 -log_files = [ - f - for f in os.listdir(data_dir) - if f.startswith("scoresight_") and f.endswith(".log") -] -# sort log files by date -log_files.sort() -if len(log_files) > 10: - for f in log_files[:-10]: - try: - os.remove(os.path.join(data_dir, f)) - except PermissionError as e: - logger.error(f"Failed to remove log file: {f}") + +def setup_logging(): + # Load the environment variables from the .env file + load_dotenv(os.path.abspath(os.path.join(os.path.dirname(__file__), ".env"))) + + # Create a logger + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + + # get the user data directory + data_dir = user_log_dir("scoresight") + if not os.path.exists(data_dir): + os.makedirs(data_dir) + + current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + + # basic config - send all logs to a file + logging.basicConfig( + filename=os.path.join(data_dir, f"scoresight_std_{current_time}.log"), + level=logging.INFO, + ) + + # prepend the user data directory + log_file_path = os.path.join(data_dir, f"scoresight_{current_time}.log") + + # Create a file handler + file_handler = logging.FileHandler(log_file_path) + file_handler.setLevel(logging.DEBUG) + + # Create a formatter + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(module)s - %(message)s" + ) + file_handler.setFormatter(formatter) + + # Add the file handler to the logger + logger.addHandler(file_handler) + + # if the .env file has a debug flag, set the logger to output to console + if os.getenv("SCORESIGHT_DEBUG"): + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + logger.debug("Debug mode enabled") + + # check to see if there are more log files, and only keep the most recent 10 + log_files = [ + f + for f in os.listdir(data_dir) + if f.startswith("scoresight_") and f.endswith(".log") + ] + # sort log files by date + log_files.sort() + if len(log_files) > 10: + for f in log_files[:-10]: + try: + os.remove(os.path.join(data_dir, f)) + except PermissionError as e: + logger.error(f"Failed to remove log file: {f}") + + return logger, file_handler, log_file_path + + +try: + # Create a logger + logger, file_handler, log_file_path = setup_logging() +except Exception as e: + print(f"Error setting up logging: {e}") diff --git a/ui_mainwindow.py b/ui_mainwindow.py index ae9f8c8..ced6658 100644 --- a/ui_mainwindow.py +++ b/ui_mainwindow.py @@ -774,6 +774,7 @@ def setupUi(self, MainWindow): self.comboBox_api_encode.addItem("") self.comboBox_api_encode.addItem("") self.comboBox_api_encode.addItem("") + self.comboBox_api_encode.addItem("") self.comboBox_api_encode.setObjectName(u"comboBox_api_encode") self.formLayout_3.setWidget(4, QFormLayout.FieldRole, self.comboBox_api_encode) @@ -793,11 +794,14 @@ def setupUi(self, MainWindow): self.horizontalLayout_27.addWidget(self.lineEdit_api_url) - self.checkBox_is_websocket = QCheckBox(self.widget_24) - self.checkBox_is_websocket.setObjectName(u"checkBox_is_websocket") - self.checkBox_is_websocket.setEnabled(False) + self.comboBox_outApiMethod = QComboBox(self.widget_24) + self.comboBox_outApiMethod.addItem("") + self.comboBox_outApiMethod.addItem("") + self.comboBox_outApiMethod.addItem("") + self.comboBox_outApiMethod.setObjectName(u"comboBox_outApiMethod") + self.comboBox_outApiMethod.setMaximumSize(QSize(50, 16777215)) - self.horizontalLayout_27.addWidget(self.checkBox_is_websocket) + self.horizontalLayout_27.addWidget(self.comboBox_outApiMethod) self.formLayout_3.setWidget(1, QFormLayout.FieldRole, self.widget_24) @@ -1223,15 +1227,16 @@ def retranslateUi(self, MainWindow): self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_vmix), QCoreApplication.translate("MainWindow", u"VMix", None)) self.checkBox_enableOutAPI.setText(QCoreApplication.translate("MainWindow", u"Send out API requests to external services.", None)) self.label_21.setText(QCoreApplication.translate("MainWindow", u"Encode", None)) - self.comboBox_api_encode.setItemText(0, QCoreApplication.translate("MainWindow", u"JSON", None)) - self.comboBox_api_encode.setItemText(1, QCoreApplication.translate("MainWindow", u"XML", None)) - self.comboBox_api_encode.setItemText(2, QCoreApplication.translate("MainWindow", u"CSV", None)) + self.comboBox_api_encode.setItemText(0, QCoreApplication.translate("MainWindow", u"JSON (Full)", None)) + self.comboBox_api_encode.setItemText(1, QCoreApplication.translate("MainWindow", u"JSON (Simple key-value)", None)) + self.comboBox_api_encode.setItemText(2, QCoreApplication.translate("MainWindow", u"XML", None)) + self.comboBox_api_encode.setItemText(3, QCoreApplication.translate("MainWindow", u"CSV", None)) self.lineEdit_api_url.setPlaceholderText(QCoreApplication.translate("MainWindow", u"http://", None)) -#if QT_CONFIG(tooltip) - self.checkBox_is_websocket.setToolTip(QCoreApplication.translate("MainWindow", u"Not implemented yet.", None)) -#endif // QT_CONFIG(tooltip) - self.checkBox_is_websocket.setText(QCoreApplication.translate("MainWindow", u"Websocket", None)) + self.comboBox_outApiMethod.setItemText(0, QCoreApplication.translate("MainWindow", u"POST", None)) + self.comboBox_outApiMethod.setItemText(1, QCoreApplication.translate("MainWindow", u"PUT", None)) + self.comboBox_outApiMethod.setItemText(2, QCoreApplication.translate("MainWindow", u"GET", None)) + self.label_20.setText(QCoreApplication.translate("MainWindow", u"URL", None)) self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_api), QCoreApplication.translate("MainWindow", u"API", None)) self.pushButton_stopUpdates.setText(QCoreApplication.translate("MainWindow", u"Stop Updates", None))