From 1a511ff54feb263da2b3c68bc88483b650312a67 Mon Sep 17 00:00:00 2001 From: Lowa Date: Mon, 17 Feb 2025 00:53:24 +0300 Subject: [PATCH] Enhance update checking - Refactor UpdateChecker - Add support for parsing release details with improved formatting - Implement more robust version comparison and release type handling - Add logging for update checking process - Improve error handling and release information extraction - Update update checking logic to handle stable and pre-release versions --- ComConfigCopy.py | 42 +++++-- about_window.py | 2 +- update_checker.py | 280 ++++++++++++++++++++++------------------------ 3 files changed, 167 insertions(+), 157 deletions(-) diff --git a/ComConfigCopy.py b/ComConfigCopy.py index 9756c75..3c50979 100644 --- a/ComConfigCopy.py +++ b/ComConfigCopy.py @@ -524,15 +524,16 @@ class SettingsWindow(tk.Toplevel): copy_mode_frame = ttk.Frame(settings_frame) copy_mode_frame.grid(row=3, column=1, sticky=W, pady=5) ttk.Radiobutton(copy_mode_frame, text="Построчно", value="line", - variable=self.copy_mode_var).pack(side=LEFT) + variable=self.copy_mode_var, command=self.toggle_block_size).pack(side=LEFT) ttk.Radiobutton(copy_mode_frame, text="Блоками", value="block", - variable=self.copy_mode_var).pack(side=LEFT) + variable=self.copy_mode_var, command=self.toggle_block_size).pack(side=LEFT) # Размер блока - ttk.Label(settings_frame, text="Размер блока:").grid(row=4, column=0, sticky=W, pady=5) + self.block_size_label = ttk.Label(settings_frame, text="Размер блока:") + self.block_size_label.grid(row=4, column=0, sticky=W, pady=5) self.block_size_var = StringVar(value=str(settings.get("block_size", 15))) - block_size_entry = CustomEntry(settings_frame, textvariable=self.block_size_var) - block_size_entry.grid(row=4, column=1, sticky=W, pady=5) + self.block_size_entry = CustomEntry(settings_frame, textvariable=self.block_size_var) + self.block_size_entry.grid(row=4, column=1, sticky=W, pady=5) # Приглашение командной строки ttk.Label(settings_frame, text="Приглашение:").grid(row=5, column=0, sticky=W, pady=5) @@ -548,9 +549,21 @@ class SettingsWindow(tk.Toplevel): self.update_ports() + # Инициализация видимости поля размера блока + self.toggle_block_size() + # Центрируем окно self.center_window() - + + # Переключение видимости поля размера блока + def toggle_block_size(self): + if self.copy_mode_var.get() == "line": + self.block_size_label.grid_remove() + self.block_size_entry.grid_remove() + else: + self.block_size_label.grid() + self.block_size_entry.grid() + # Центрирование окна def center_window(self): self.update_idletasks() @@ -670,7 +683,8 @@ class SerialAppGUI(tk.Tk): # Инициализация проверки обновлений self.update_checker = UpdateChecker( VERSION, - "https://gitea.filow.ru/LowaSC/ComConfigCopy" + "https://gitea.filow.ru/LowaSC/ComConfigCopy", + include_prereleases=False # Проверяем только стабильные версии ) # Настройка стиля @@ -719,14 +733,20 @@ class SerialAppGUI(tk.Tk): elif update_available: release_info = self.update_checker.get_release_notes() if release_info: + # Форматируем сообщение + message = ( + f"Доступна новая версия {release_info['version']}!\n\n" + f"Тип релиза: {'Пре-релиз' if release_info['type'] == 'prerelease' else 'Стабильный'}\n\n" + "Изменения:\n" + f"{release_info['description']}\n\n" + "Хотите перейти на страницу загрузки?" + ) response = messagebox.askyesno( "Доступно обновление", - f"Доступна новая версия {release_info['version']}!\n\n" - f"Изменения:\n{release_info['description']}\n\n" - "Хотите перейти на страницу загрузки?", + message, ) if response: - webbrowser.open(release_info["download_url"]) + webbrowser.open(release_info["link"]) else: messagebox.showerror( "Ошибка", diff --git a/about_window.py b/about_window.py index 1d3240e..911ced4 100644 --- a/about_window.py +++ b/about_window.py @@ -69,7 +69,7 @@ class AboutWindow(tk.Toplevel): ttk.Label( contact_frame, - text="Email: LowaWorkMail@gmail.com" + text="Email: SPRF555@gmail.com" ).pack(anchor="w") telegram_link = ttk.Label( diff --git a/update_checker.py b/update_checker.py index 137bf09..c1c89a5 100644 --- a/update_checker.py +++ b/update_checker.py @@ -1,175 +1,165 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import json import logging import requests import threading +import re from packaging import version +import xml.etree.ElementTree as ET +import html class UpdateCheckError(Exception): """Исключение для ошибок проверки обновлений""" pass +class ReleaseType: + """Типы релизов""" + STABLE = "stable" + PRERELEASE = "prerelease" + class UpdateChecker: """Класс для проверки обновлений программы""" - def __init__(self, current_version, repo_url): + def __init__(self, current_version, repo_url, include_prereleases=False): self.current_version = current_version self.repo_url = repo_url - # Формируем базовый URL API - self.api_url = repo_url.replace("gitea.filow.ru", "gitea.filow.ru/api/v1/repos/LowaSC/ComConfigCopy") - self._update_available = False - self._latest_version = None - self._latest_release = None - self._error = None - self._changelog = None - - def get_changelog(self, callback=None): - """ - Получение changelog из репозитория. - :param callback: Функция обратного вызова, которая будет вызвана после получения changelog - """ - def fetch(): - try: - # Пытаемся получить CHANGELOG.md из репозитория - response = requests.get(f"{self.api_url}/contents/CHANGELOG.md", timeout=10) - response.raise_for_status() - - content = response.json() - if "content" in content: - import base64 - changelog_content = base64.b64decode(content["content"]).decode("utf-8") - self._changelog = changelog_content - self._error = None - else: - raise UpdateCheckError("Не удалось получить содержимое CHANGELOG.md") - - except requests.RequestException as e: - error_msg = f"Ошибка получения changelog: {e}" - logging.error(error_msg, exc_info=True) - self._error = error_msg - self._changelog = None - except Exception as e: - error_msg = f"Неизвестная ошибка при получении changelog: {e}" - logging.error(error_msg, exc_info=True) - self._error = error_msg - self._changelog = None - finally: - if callback: - callback(self._changelog, self._error) + self.include_prereleases = include_prereleases + self.rss_url = f"{repo_url}/releases.rss" + self.release_info = None + + def _clean_html(self, html_text): + """Очищает HTML-разметку и форматирует текст""" + if not html_text: + return "" + text = re.sub(r'<[^>]+>', '', html_text) + text = html.unescape(text) + text = re.sub(r'\n\s*\n', '\n\n', text) + return '\n'.join(line.strip() for line in text.splitlines()).strip() + + def _parse_release_info(self, item): + """Извлекает информацию о релизе из RSS item""" + title = item.find('title').text if item.find('title') is not None else '' + link = item.find('link').text if item.find('link') is not None else '' + description = item.find('description').text if item.find('description') is not None else '' + content = item.find('{http://purl.org/rss/1.0/modules/content/}encoded') + content_text = content.text if content is not None else '' - # Запускаем получение в отдельном потоке - threading.Thread(target=fetch, daemon=True).start() - + # Извлекаем версию и проверяем тип релиза из тега + version_match = re.search(r'/releases/tag/(?:pre-)?v?(\d+\.\d+(?:\.\d+)?)', link) + if not version_match: + return None + + version_str = version_match.group(1) + # Проверяем наличие префикса pre- в теге + is_prerelease = 'pre-' in link.lower() + + # Форматируем название релиза + formatted_title = title + if title == version_str or not title.strip(): + # Если заголовок пустой или совпадает с версией, создаем стандартное название + release_type = "Пре-релиз" if is_prerelease else "Версия" + formatted_title = f"{release_type} {version_str}" + elif not re.search(version_str, title): + # Если версия не указана в заголовке, добавляем её + formatted_title = f"{title} ({version_str})" + + # Форматируем описание + formatted_description = self._clean_html(content_text or description) + if not formatted_description.strip(): + formatted_description = "Нет описания" + + # Добавляем метку типа релиза в начало описания + release_type_label = "[Пре-релиз] " if is_prerelease else "" + formatted_description = f"{release_type_label}{formatted_description}" + + return { + 'title': formatted_title, + 'link': link, + 'description': formatted_description, + 'version': version_str, + 'type': ReleaseType.PRERELEASE if is_prerelease else ReleaseType.STABLE + } + def check_updates(self, callback=None): - """ - Проверка наличия обновлений. - :param callback: Функция обратного вызова, которая будет вызвана после проверки - """ - def check(): + """Проверяет наличие обновлений в асинхронном режиме""" + def check_worker(): try: - response = requests.get(f"{self.api_url}/releases", timeout=10) + logging.info(f"Текущая версия программы: {self.current_version}") + logging.info(f"Проверка пре-релизов: {self.include_prereleases}") + logging.info(f"Запрос RSS ленты: {self.rss_url}") + + response = requests.get(self.rss_url, timeout=10) response.raise_for_status() - releases = response.json() - if not releases: - raise UpdateCheckError("Не найдено релизов в репозитории") + root = ET.fromstring(response.content) + items = root.findall('.//item') + if not items: + raise UpdateCheckError("Релизы не найдены") - latest_release = releases[0] - latest_version = latest_release.get("tag_name", "").lstrip("v") + logging.info(f"Найдено {len(items)} релизов") - if not latest_version: - raise UpdateCheckError("Не удалось определить версию последнего релиза") + latest_version = None + latest_info = None - try: - if version.parse(latest_version) > version.parse(self.current_version): - self._update_available = True - self._latest_version = latest_version - self._latest_release = latest_release - logging.info(f"Доступно обновление: {latest_version}") - else: - logging.info("Обновления не требуются") - except version.InvalidVersion as e: - raise UpdateCheckError(f"Некорректный формат версии: {e}") + for item in items: + release_info = self._parse_release_info(item) + if not release_info: + continue + + is_prerelease = release_info['type'] == ReleaseType.PRERELEASE + logging.info( + f"Проверка релиза: {release_info['title']}, " + f"версия: {release_info['version']}, " + f"тип: {'пре-релиз' if is_prerelease else 'стабильная'}" + ) - self._error = None + # Пропускаем пре-релизы если они не включены + if is_prerelease and not self.include_prereleases: + logging.info(f"Пропуск пре-релиза: {release_info['version']}") + continue - except requests.RequestException as e: - error_msg = f"Ошибка сетевого подключения: {e}" - logging.error(error_msg, exc_info=True) - self._error = error_msg - except UpdateCheckError as e: - logging.error(str(e), exc_info=True) - self._error = str(e) - except Exception as e: - error_msg = f"Неизвестная ошибка при проверке обновлений: {e}" - logging.error(error_msg, exc_info=True) - self._error = error_msg - finally: + # Сравниваем версии + try: + current_ver = version.parse(latest_version or "0.0.0") + new_ver = version.parse(release_info['version'].split('-')[0]) # Убираем суффикс для сравнения + + if new_ver > current_ver: + latest_version = release_info['version'] + latest_info = release_info + logging.info(f"Новая версия: {latest_version}") + + except version.InvalidVersion as e: + logging.warning(f"Некорректный формат версии {release_info['version']}: {e}") + continue + + if not latest_info: + raise UpdateCheckError("Не найдены подходящие версии") + + self.release_info = latest_info + + # Сравниваем с текущей версией + current_ver = version.parse(self.current_version) + latest_ver = version.parse(latest_version.split('-')[0]) + update_available = latest_ver > current_ver + + logging.info(f"Сравнение версий: текущая {current_ver} <-> последняя {latest_ver}") + logging.info(f"Доступно обновление: {update_available}") + if callback: - callback(self._update_available, self._error) - - @property - def update_available(self): - """Доступно ли обновление""" - return self._update_available - - @property - def latest_version(self): - """Последняя доступная версия""" - return self._latest_version - - @property - def error(self): - """Последняя ошибка при проверке обновлений""" - return self._error - - @property - def changelog(self): - """Текущий changelog""" - return self._changelog + callback(update_available, None) + + except UpdateCheckError as e: + logging.error(str(e)) + if callback: + callback(False, str(e)) + except Exception as e: + logging.error(f"Ошибка при проверке обновлений: {e}", exc_info=True) + if callback: + callback(False, str(e)) + + threading.Thread(target=check_worker, daemon=True).start() def get_release_notes(self): - """Получение информации о последнем релизе""" - if self._latest_release: - return { - "version": self._latest_version, - "description": self._latest_release.get("body", ""), - "download_url": self._latest_release.get("assets", [{}])[0].get("browser_download_url", "") - } - return None - - def get_releases(self, callback=None): - """ - Получение списка релизов из репозитория. - :param callback: Функция обратного вызова, которая будет вызвана после получения списка релизов - """ - def fetch(): - try: - response = requests.get(f"{self.api_url}/releases", timeout=10) - response.raise_for_status() - releases = response.json() - - if not releases: - raise UpdateCheckError("Не найдено релизов в репозитории") - - self._error = None - if callback: - callback(releases, None) - - except requests.RequestException as e: - error_msg = f"Ошибка сетевого подключения: {e}" - logging.error(error_msg, exc_info=True) - self._error = error_msg - if callback: - callback(None, error_msg) - except Exception as e: - error_msg = f"Ошибка при получении списка релизов: {e}" - logging.error(error_msg, exc_info=True) - self._error = error_msg - if callback: - callback(None, error_msg) - - # Запускаем получение в отдельном потоке - threading.Thread(target=fetch, daemon=True).start() \ No newline at end of file + """Возвращает информацию о последнем релизе""" + return self.release_info \ No newline at end of file