From 3fb1475d72318254c0f6e208ac24241119e9b6c9 Mon Sep 17 00:00:00 2001 From: jan Haberlau Date: Sat, 13 Jul 2024 21:20:41 +0200 Subject: [PATCH] added workflow --- .github/workflows/pylint.yml | 33 +++---- .idea/discord.xml | 2 +- .idea/misc.xml | 3 + last_sent_changelog.json | 9 ++ main.py | 103 +++++++++++++++++++++ modules/changelog_parser.py | 170 +++++++++++++++++++++++++++++++++++ modules/console_color.py | 56 ++++++++++++ modules/discord_notifier.py | 69 ++++++++++++++ 8 files changed, 429 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index c73e032..a8e7d37 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -3,21 +3,24 @@ name: Pylint on: [push] jobs: - build: - runs-on: ubuntu-latest + build-windows: + runs-on: windows-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.9"] steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint - - name: Analysing the code with pylint - run: | - pylint $(git ls-files '*.py') + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint beautifulsoup4 requests + + - name: Analysing the code with pylint + run: | + Get-ChildItem -Path . -Filter *.py -Recurse | ForEach-Object { pylint $_.FullName --max-line-length=140 --disable=R0903 } diff --git a/.idea/discord.xml b/.idea/discord.xml index 30bab2a..d8e9561 100644 --- a/.idea/discord.xml +++ b/.idea/discord.xml @@ -1,7 +1,7 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 5523336..34baf2b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ + + \ No newline at end of file diff --git a/last_sent_changelog.json b/last_sent_changelog.json index e69de29..55c0a4c 100644 --- a/last_sent_changelog.json +++ b/last_sent_changelog.json @@ -0,0 +1,9 @@ +{ + "version": "2.0.2.7", + "date": "2024-07-03", + "changelog": [ + "Behoben: Das Problem der Verz\u00f6gerung beim Videowiedergabeergebnis der Video-Passthrough-Funktion des Konvertermoduls wurde behoben.", + "Behoben: Das Problem, dass die KI-Modulkonvertierung beim Herunterladen und Ausf\u00fchren der Umgebung h\u00e4ngenbleibt.", + "Behoben: Einige Probleme mit der Anzeige der Benutzeroberfl\u00e4che." + ] +} \ No newline at end of file diff --git a/main.py b/main.py index e69de29..110231d 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,103 @@ +""" +Module: changelog_watcher + +This module implements the ChangelogWatcher class, which monitors a specified URL for updates, +parses the changelog, and sends notifications to a Discord webhook. +""" + +import time +from modules.console_color import Color # Import the Color class for color formatting +from modules.changelog_parser import ChangelogParser +from modules.discord_notifier import DiscordNotifier + + +class ChangelogWatcher: + """ + ChangelogWatcher class monitors a specified URL for updates, + parses the changelog, and sends notifications to a Discord webhook. + """ + + def __init__(self, arg_url, arg_webhook_url): + """ + Initializes the ChangelogWatcher instance with the provided URL and Discord webhook URL. + + Args: + arg_url (str): The URL to fetch the changelog from. + arg_webhook_url (str): The URL of the Discord webhook to send notifications to. + """ + self.url = arg_url + self.webhook_url = arg_webhook_url + self.discord_notifier = DiscordNotifier(arg_webhook_url) + self.changelog_parser = ChangelogParser() + self.last_sent_changelog = self.changelog_parser.load_last_sent_changelog() + + def fetch_changelog(self): + """ + Fetches the changelog HTML content from the specified URL. + + Returns: + str or None: The fetched HTML content if successful, None if an error occurs. + """ + return self.changelog_parser.fetch_changelog(self.url) + + def parse_changelog(self, arg_html_content): + """ + Parses the HTML content of the changelog to extract version, date, and changelog items. + + Args: + arg_html_content (str): The HTML content of the changelog to parse. + + Returns: + dict or None: A dictionary containing version, date, and changelog items + if parsing is successful, None if an error occurs. + """ + return self.changelog_parser.parse_changelog(arg_html_content) + + def send_to_discord_webhook(self, arg_software_name, arg_changelog_data): + """ + Sends the parsed changelog data to the Discord webhook. + + Args: + arg_software_name (str): The name of the software for the update. + arg_changelog_data (dict): The parsed changelog data to send. + """ + self.discord_notifier.send_to_discord_webhook(arg_software_name, arg_changelog_data) + + def run(self): + """ + Runs the ChangelogWatcher to continuously monitor the changelog, parse updates, + and send notifications to Discord. + """ + while True: + html_content = self.fetch_changelog() + if html_content: + changelog_data = self.parse_changelog(html_content) + + if changelog_data: + if changelog_data != self.last_sent_changelog: + self.send_to_discord_webhook("UniFab", changelog_data) + self.last_sent_changelog = changelog_data + self.changelog_parser.save_last_sent_changelog(changelog_data) # Save immediately + else: + self.last_sent_changelog = changelog_data + self.changelog_parser.save_last_sent_changelog(changelog_data) # Save immediately + else: + print(Color.red("No valid Update data found.")) + else: + print(Color.red("Failed to fetch Update data.")) + + time.sleep(20) # 20 seconds + + +if __name__ == "__main__": + URL = 'https://de.unifab.ai/unifab-new.htm' # Replace with the actual URL + WEBHOOK_URL = ('WEBHOOK URL HERE') + SOFTWARE_NAME = 'UniFab Update Notificator' + AUTHOR_NAME = 'clientinfo' + + # Print software name in green and author name in blue + print(f"{Color.green('Software:')} {Color.blue(SOFTWARE_NAME)}") + print(f"{Color.green('Creator:')} {Color.blue(AUTHOR_NAME)}") + + watcher = ChangelogWatcher(URL, WEBHOOK_URL) + watcher.run() diff --git a/modules/changelog_parser.py b/modules/changelog_parser.py index e69de29..ec82967 100644 --- a/modules/changelog_parser.py +++ b/modules/changelog_parser.py @@ -0,0 +1,170 @@ +""" +Module: changelog_parser + +This module provides functionality to fetch, parse, save, and load changelog data +from HTML and JSON files. +""" + +import json +import os +from datetime import datetime +import requests +from bs4 import BeautifulSoup +from modules.console_color import Color # Import the Color class from console_color.py + + +class ChangelogParser: + """ + A class to fetch, parse, save, and load changelog data from HTML and JSON files. + + Methods: + fetch_changelog(arg_url): + Fetches HTML content from a given URL. + + parse_changelog(arg_html_content): + Parses HTML content to extract changelog information. + + save_last_sent_changelog(arg_changelog_data): + Saves parsed changelog data to a JSON file. + + load_last_sent_changelog(): + Loads previously saved changelog data from a JSON file. + """ + + @staticmethod + def fetch_changelog(arg_url): + """ + Fetches HTML content from a given URL using requests. + + Args: + arg_url (str): The URL to fetch HTML content from. + + Returns: + str: The HTML content fetched from the URL, or None if there was an error. + """ + try: + response = requests.get(arg_url, timeout=10) # Added timeout argument + response.raise_for_status() # Raise an error for bad status codes + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('Fetching URL:')} {arg_url}") + return response.text + except requests.exceptions.RequestException as e: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('Error fetching the URL:')} {e}") + return None + + @staticmethod + def parse_changelog(arg_html_content): + """ + Parses HTML content to extract changelog information using BeautifulSoup. + + Args: + arg_html_content (str): The HTML content to parse. + + Returns: + dict or None: A dictionary containing parsed changelog data (version, date, changelog), + or None if parsing fails or no valid changelog data found. + """ + if not arg_html_content: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('Empty HTML content received.')}") + return None + + try: + soup = BeautifulSoup(arg_html_content, 'html.parser') + container = soup.find('div', class_='bg-white b-rd-8 pl40 pr40 pb32 changelog-content') + if not container: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('Container div not found.')}") + return None + + section = container.find('p', class_='whatsnew') + if not section: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('No changelog section found within the container.')}") + return None + + version = section.find('strong').text.strip() if section.find('strong') else 'N/A' + date = section.find('span').text.strip() if section.find('span') else 'N/A' + changelog_items = section.find_all('li') + changelog = [item.text.strip() for item in changelog_items] + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.green('Changelog data parsed successfully.')}") + return { + 'version': version, + 'date': date, + 'changelog': changelog + } + except ValueError as ve: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('ValueError parsing HTML content:')} {ve}") + return None + except AttributeError as ae: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('AttributeError parsing HTML content:')} {ae}") + return None + except Exception as e: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('Error parsing HTML content:')} {e}") + return None + + @staticmethod + def save_last_sent_changelog(arg_changelog_data): + """ + Saves parsed changelog data to a JSON file named 'last_sent_changelog.json'. + + Args: + arg_changelog_data (dict): The changelog data to save. + """ + try: + with open('last_sent_changelog.json', 'w', encoding='utf-8') as file: # Specified encoding + json.dump(arg_changelog_data, file, indent=4) # Update the file with indent for readability + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.green('Changelog data saved to file.')}") + except FileNotFoundError as fnfe: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('FileNotFoundError saving Changelog data:')} {fnfe}") + except IOError as ioe: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('IOError saving Changelog data:')} {ioe}") + except Exception as e: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('Error saving Changelog data:')} {e}") + + @staticmethod + def load_last_sent_changelog(): + """ + Loads previously saved changelog data from 'last_sent_changelog.json'. + + Returns: + dict or None: A dictionary containing the last sent changelog data, + or None if the file doesn't exist or cannot be loaded. + """ + try: + if os.path.exists('last_sent_changelog.json'): + with open('last_sent_changelog.json', 'r', encoding='utf-8') as file: # Specified encoding + last_sent_changelog = json.load(file) + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.green('Last sent changelog data loaded from file.')}") + return last_sent_changelog + else: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('No saved changelog data found. Creating a new file.')}") + with open('last_sent_changelog.json', 'w', encoding='utf-8') as file: # Specified encoding + json.dump(None, file) # Create an empty JSON object + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.green('Empty file created for the last sent changelog data.')}") + return None + except FileNotFoundError as fnfe: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('FileNotFoundError loading last sent changelog data:')} {fnfe}") + except IOError as ioe: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('IOError loading last sent changelog data:')} {ioe}") + except json.JSONDecodeError as je: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('JSONDecodeError loading last sent changelog data:')} {je}") + except Exception as e: + print(f"{Color.blue('[' + datetime.now().strftime('%H:%M:%S') + ']')}: " + f"{Color.red('Error loading last sent changelog data:')} {e}") + return None diff --git a/modules/console_color.py b/modules/console_color.py index e69de29..be180f9 100644 --- a/modules/console_color.py +++ b/modules/console_color.py @@ -0,0 +1,56 @@ +""" +Module: console_color + +This module defines a Color class that provides ANSI escape sequences +for color formatting in terminal outputs. +""" + + +class Color: + """ + A class providing ANSI escape sequences for color formatting in terminals. + """ + + BLUE = "\033[34m" + RED = "\033[31m" + GREEN = "\033[32m" + RESET = "\033[0m" + + @staticmethod + def blue(arg_text): + """ + Formats text in blue color. + + Args: + arg_text (str): The text to format. + + Returns: + str: The formatted text in blue color. + """ + return f"{Color.BLUE}{arg_text}{Color.RESET}" + + @staticmethod + def red(arg_text): + """ + Formats text in red color. + + Args: + arg_text (str): The text to format. + + Returns: + str: The formatted text in red color. + """ + return f"{Color.RED}{arg_text}{Color.RESET}" + + @staticmethod + def green(arg_text): + """ + Formats text in green color. + + Args: + arg_text (str): The text to format. + + Returns: + str: The formatted text in green color. + """ + return f"{Color.GREEN}{arg_text}{Color.RESET}" diff --git a/modules/discord_notifier.py b/modules/discord_notifier.py index e69de29..730f735 100644 --- a/modules/discord_notifier.py +++ b/modules/discord_notifier.py @@ -0,0 +1,69 @@ +""" +Module: discord_notifier + +This module provides functionality to send changelog updates to Discord using webhooks. +""" + +import json +from datetime import datetime +import requests +from modules.console_color import Color # Import the Color class for color formatting + + +class DiscordNotifier: + """ + A class to send changelog updates to Discord using webhooks. + + Attributes: + webhook_url (str): The URL of the Discord webhook to send messages to. + """ + + def __init__(self, arg_webhook_url): + """ + Initializes a DiscordNotifier instance. + + Args: + arg_webhook_url (str): The URL of the Discord webhook to send messages to. + """ + self.webhook_url = arg_webhook_url + + def send_to_discord_webhook(self, arg_software_name, arg_changelog_data): + """ + Sends a changelog update to Discord using a webhook. + + Args: + arg_software_name (str): The name of the software being updated. + arg_changelog_data (dict): The changelog data containing version, date, and changelog details. + + Prints a success message if the update is sent successfully, + or an error message if there are issues sending the update. + """ + current_time = datetime.now().strftime('%H:%M:%S') + if arg_changelog_data: + try: + embed_color = 0x00ff00 # Green color (change as needed) + embed = { + 'embeds': [{ + 'title': "Latest Update", + 'description': f"Software: {arg_software_name}\n\n" + f"**Version:** {arg_changelog_data['version']}\n" + f"**Date:** {arg_changelog_data['date']}\n\n" + f"**Changelog:**\n" + "\n".join( + f"- {item}" for item in arg_changelog_data['changelog']), + 'color': embed_color + }] + } + headers = { + 'Content-Type': 'application/json' + } + response = requests.post(self.webhook_url, data=json.dumps(embed), headers=headers, timeout=10) # Timeout added + response.raise_for_status() + + print( + f"{Color.blue('[' + current_time + ']')}: {Color.green('Changelog sent to Discord successfully.')}") + except requests.exceptions.RequestException as e: + print( + f"{Color.blue('[' + current_time + ']')}: {Color.red(f'Error sending changelog data to Discord: {e}')}") + else: + print( + f"{Color.blue('[' + current_time + ']')}: {Color.red('No changelog data to send.')}")