client-py/main.py
2026-05-02 14:00:59 +05:00

184 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import hashlib
import json
import requests
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from pathlib import Path
import threading
# --- КОНФИГУРАЦИЯ ---
SERVER_URL = "https://server1.borderban.ru/mods/"
CONFIG_FILE = Path.home() / ".factorio_sync_config.json"
def get_file_hash(filepath):
sha256_hash = hashlib.sha256()
with open(filepath, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
class SyncApp:
def __init__(self, root):
self.root = root
self.root.title("Factorio Mod Sync")
self.root.geometry("500x250")
self.config = self.load_config()
self.setup_ui()
def load_config(self):
if CONFIG_FILE.exists():
return json.loads(CONFIG_FILE.read_text())
return {"mods_path": ""}
def save_config(self, mods_path):
CONFIG_FILE.write_text(json.dumps({"mods_path": mods_path}))
def setup_ui(self):
# Выбор пути
tk.Label(self.root, text="Путь к папке mods:").pack(pady=(10, 0))
self.path_var = tk.StringVar(value=self.config["mods_path"])
entry_frame = tk.Frame(self.root)
entry_frame.pack(fill="x", padx=20, pady=5)
tk.Entry(entry_frame, textvariable=self.path_var).pack(side="left", fill="x", expand=True)
tk.Button(entry_frame, text="Обзор", command=self.select_path).pack(side="right", padx=5)
# Статус и Прогресс
self.status_label = tk.Label(self.root, text="Готов к работе", fg="blue")
self.status_label.pack(pady=(10, 0))
self.progress = ttk.Progressbar(self.root, orient="horizontal", length=400, mode="determinate")
self.progress.pack(pady=10)
self.sync_btn = tk.Button(self.root, text="СИНХРОНИЗИРОВАТЬ", command=self.start_sync_thread,
bg="#2c3e50", fg="white", font=("Arial", 10, "bold"), height=2)
self.sync_btn.pack(pady=10)
def select_path(self):
path = filedialog.askdirectory()
if path:
self.path_var.set(path)
self.save_config(path)
def update_status(self, text, value=None):
self.status_label.config(text=text)
if value is not None:
self.progress["value"] = value
self.root.update_idletasks()
def start_sync_thread(self):
# Запускаем в отдельном потоке, чтобы GUI не фризился
thread = threading.Thread(target=self.run_sync)
thread.start()
def run_sync(self):
mods_path = self.path_var.get()
if not mods_path:
messagebox.showwarning("Внимание", "Сначала выберите папку с модами!")
return
self.sync_btn.config(state="disabled")
try:
# 1. Получение манифеста
self.update_status("Получение списка модов с сервера...", 5)
response = requests.get(f"{SERVER_URL}mods_list.json", timeout=15)
response.raise_for_status()
server_manifest = response.json()
local_mods = Path(mods_path)
if not local_mods.exists():
local_mods.mkdir(parents=True, exist_ok=True)
mod_status = {}
# --- ЭТАП ОЧИСТКИ (Удаление лишнего) ---
self.update_status("Очистка старых модов...", 10)
local_files = list(local_mods.glob("*.zip"))
for local_file in local_files:
if local_file.name not in server_manifest:
print(f"Удаление лишнего файла: {local_file.name}")
local_file.unlink()
mod_status[local_file.name] = "удалено"
# ---------------------------------------
# 2. Синхронизация (Загрузка нужного)
total_mods = len(server_manifest)
current_mod_idx = 0
for filename, server_hash in server_manifest.items():
current_mod_idx += 1
self.update_status(f"Синхронизация: {current_mod_idx}/{total_mods}",
20 + (current_mod_idx / total_mods) * 80)
local_file = local_mods / filename
file_exists = local_file.exists()
if not file_exists:
needs_download = True
mod_status[filename] = "добавлено"
elif get_file_hash(local_file) != server_hash:
needs_download = True
mod_status[filename] = "обновлено"
else:
needs_download = False
mod_status[filename] = "без изменений"
if needs_download:
self.update_status(f"Загрузка: {filename}...", self.progress["value"])
with requests.get(f"{SERVER_URL}{filename}", stream=True) as r:
r.raise_for_status()
with open(local_file, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
self.update_status("Синхронизация завершена!", 100)
# --- СОРТИРОВКА И ВЫВОД ОТЧЕТА ---
sort_order = {
"без изменений": 1,
"обновлено": 2,
"добавлено": 3,
"удалено": 4
}
sorted_mods = sorted(mod_status.items(), key=lambda item: sort_order.get(item[1], 5))
# Безопасный вызов отрисовки UI из фонового потока
self.root.after(0, self.show_report, sorted_mods)
except Exception as e:
self.update_status("Ошибка!", 0)
self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e)))
finally:
self.root.after(0, lambda: self.sync_btn.config(state="normal"))
def show_report(self, sorted_mods):
report_win = tk.Toplevel(self.root)
report_win.title("Отчет о синхронизации")
report_win.geometry("400x300") # Компактный размер
# Текстовое поле с темным фоном для читаемости цветного текста
text_widget = tk.Text(report_win, font=("Consolas", 10), bg="#1e1e1e", fg="white", wrap="none")
scrollbar_y = ttk.Scrollbar(report_win, orient="vertical", command=text_widget.yview)
text_widget.configure(yscrollcommand=scrollbar_y.set)
scrollbar_y.pack(side="right", fill="y")
text_widget.pack(side="left", fill="both", expand=True, padx=5, pady=5)
# Настраиваем цветовые теги
text_widget.tag_config("добавлено", foreground="#55ff55") # Светло-зеленый
text_widget.tag_config("удалено", foreground="#ff5555") # Светло-красный
text_widget.tag_config("обновлено", foreground="#ffff55") # Желтый
text_widget.tag_config("без изменений", foreground="#888888") # Серый
# Заполняем поле
for mod, status in sorted_mods:
text_widget.insert(tk.END, f"{mod}: ")
text_widget.insert(tk.END, f"{status}\n", status)
text_widget.config(state="disabled") # Только чтение
if __name__ == "__main__":
root = tk.Tk()
app = SyncApp(root)
root.mainloop()