from flask import Flask, render_template, request, jsonify, send_from_directory, redirect, url_for from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user from werkzeug.security import generate_password_hash, check_password_hash import re from euro_scraper import create_retry_session, fetch_products, setup_selenium import os import threading from datetime import datetime import json from translator import ProductTranslator from feed_generator import RobotVacuumYMLGenerator from pathlib import Path from werkzeug.exceptions import NotFound from urllib.parse import urljoin BASE_URL = "https://mario.mrakells.pp.ua" # Добавляем в начало файла login_manager = LoginManager() login_manager.login_view = 'login' class User(UserMixin): def __init__(self, id, username, password_hash): self.id = id self.username = username self.password_hash = password_hash # Хранилище пользователей (в реальном приложении использовать базу данных) users = { 'mario': User(1, 'mario', generate_password_hash('2htC9YlEMXAhNE')) } @login_manager.user_loader def load_user(user_id): for user in users.values(): if user.id == int(user_id): return user return None app = Flask(__name__) # Добавляем после создания app login_manager.init_app(app) app.config['SECRET_KEY'] = 'your-secret-key-here' # Замените на случайный ключ # Глобальные настройки app_settings = {"items_limit": -1} # Ограничение количества обрабатываемых товаров # Глобальная переменная для хранения статуса перевода translation_status = { "is_running": False, "total_items": 0, "processed_items": 0, "error": None, } # Добавить в начало файла CATEGORIES_FILE = "categories.json" # Создаем константы для путей OUTPUT_DIR = Path("output") TRANSLATED_DIR = OUTPUT_DIR / "translated" YML_DIR = OUTPUT_DIR / "yml" # Глобальное состояние парсинга parsing_status = { "is_running": False, "total_items": 0, "processed_items": 0, "error": None, } def load_categories(): """Загрузка категорий из файла""" if os.path.exists(CATEGORIES_FILE): with open(CATEGORIES_FILE, "r", encoding="utf-8") as f: return json.load(f) return [] def save_categories(categories): """Сохранение категорий в файл""" with open(CATEGORIES_FILE, "w", encoding="utf-8") as f: json.dump(categories, f, ensure_ascii=False, indent=2) def extract_category(url: str) -> str: """Извлекает название категории из URL""" # Пример URL: https://www.euro.com.pl/odkurzacze-automatyczne.bhtml match = re.search(r"euro\.com\.pl/([^/]+)", url) if match: category = match.group(1).replace(".bhtml", "") return category return None def start_parsing(category): """Запуск парсинга категории""" global parsing_status try: parsing_status.update( {"is_running": True, "total_items": 0, "processed_items": 0, "error": None} ) # Создаем сессию и драйвер Selenium session = create_retry_session() driver = setup_selenium() try: # Парсим с использованием драйвера products = fetch_products(category, session, driver, parsing_status) # Сохраняем результаты if products: output_file = os.path.join("output", f"{category}_products.json") with open(output_file, "w", encoding="utf-8") as f: json.dump(products, f, ensure_ascii=False, indent=2) finally: # Обязательно закрываем драйвер driver.quit() except Exception as e: parsing_status["error"] = str(e) print(f"Error during parsing: {e}") finally: parsing_status["is_running"] = False def get_file_info(filename, directory="output"): """Получение информации о файле""" filepath = os.path.join(directory, filename) stat = os.stat(filepath) return { "name": filename, "modified": datetime.fromtimestamp(stat.st_mtime).strftime("%d.%m.%Y %H:%M:%S"), "size": f"{stat.st_size / 1024:.1f} KB", } def get_oldest_parsed_file(): """Повертає найстаріший _products.json файл""" folder = "output" files = [ f for f in os.listdir(folder) if f.endswith("_products.json") and not f.endswith("_translated_products.json") ] if not files: return None oldest_file = min(files, key=lambda f: os.path.getmtime(os.path.join(folder, f))) category = oldest_file.replace("_products.json", "") return category @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') user = users.get(username) if user and check_password_hash(user.password_hash, password): login_user(user) return redirect(url_for('index')) return render_template('login.html', error='Неверный логин или пароль') return render_template('login.html') @app.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('login')) @app.route("/") @login_required def index(): """Главная страница""" # Получаем спарсенные файлы parsed_files = [] if os.path.exists("output"): files = [f for f in os.listdir("output") if f.endswith("_products.json")] parsed_files = [get_file_info(f, "output") for f in files] # Получаем переведенные файлы translated_files = [] if os.path.exists("output/translated"): files = [ f for f in os.listdir("output/translated") if f.endswith("_translated_products.json") ] translated_files = [get_file_info(f, "output/translated") for f in files] # Получаем YML файлы yml_files = [] if os.path.exists("output/yml"): files = [f for f in os.listdir("output/yml") if f.endswith(".yml")] yml_files = [get_file_info(f, "output/yml") for f in files] # Загружаем категории categories = load_categories() return render_template( "index.html", status=parsing_status, translation_status=translation_status, parsed_files=parsed_files, translated_files=translated_files, yml_files=yml_files, categories=categories, app_settings=app_settings, ) @app.route("/auto-refresh", methods=["POST"]) def auto_refresh(): """Запускає парсинг найстарішої збереженої категорії""" if parsing_status["is_running"]: return jsonify({"error": "Парсинг уже запущено"}) category = get_oldest_parsed_file() if not category: return jsonify({"error": "Немає жодної категорії для оновлення"}) print(f"[AUTO REFRESH] Повторний парсинг для категорії: {category}") thread = threading.Thread(target=start_parsing, args=(category,)) thread.start() return jsonify({"success": True, "category": category}) @app.route("/parse", methods=["POST"]) @login_required def parse(): """Обработчик запуска парсинга""" url = request.form.get("url") if not url: return jsonify({"error": "URL не указан"}) category = extract_category(url) if not category: return jsonify({"error": "Неверный формат URL"}) if parsing_status["is_running"]: return jsonify({"error": "Парсинг уже запущен"}) # Запускаем парсинг в отдельном потоке thread = threading.Thread(target=start_parsing, args=(category,)) thread.start() return jsonify({"status": "ok"}) @app.route("/status") def get_status(): """Получение статуса парсинга""" return jsonify(parsing_status) @app.route("/download/") def download_file(filename): """Скачивание файла с результатами""" directory = request.args.get( "directory", "output" ) # Получаем директорию из параметров запроса if directory == "translated": directory = "output/translated" elif directory == "yml": directory = "output/yml" else: directory = "output" return send_from_directory(directory, filename, as_attachment=True) @app.route("/delete/", methods=["POST"]) def delete_file(filename): """Удаление файла""" try: directory = request.args.get("directory", "output") if directory == "translated": file_path = os.path.join("output/translated", filename) elif directory == "yml": file_path = os.path.join("output/yml", filename) else: file_path = os.path.join("output", filename) if os.path.exists(file_path): os.remove(file_path) return jsonify({"success": True}) else: return jsonify({"error": "Файл не найден"}), 404 except Exception as e: return jsonify({"error": str(e)}), 400 @app.route("/translate", methods=["POST"]) def translate(): """Обработчик запуска перевода""" if translation_status["is_running"]: return jsonify({"error": "Перевод уже запущен"}) filename = request.form.get("filename") if not filename: return jsonify({"error": "Файл не выбран"}) # Запускаем перевод в отдельном потоке thread = threading.Thread(target=start_translation, args=(filename,)) thread.start() return jsonify({"status": "Перевод запущен"}) @app.route("/translation-status") def get_translation_status(): """Возвращает текущий статус перевода""" return jsonify(translation_status) def start_translation(filename: str): """Функция для запуска перевода в отдельнм потоке""" global translation_status translation_status["is_running"] = True translation_status["processed_items"] = 0 translation_status["error"] = None try: os.makedirs("output/translated", exist_ok=True) with open(os.path.join("output", filename), "r", encoding="utf-8") as f: products = json.load(f) # Ограничиваем количество товаров только если лимит больше 0 if app_settings["items_limit"] > 0: products = products[: app_settings["items_limit"]] translation_status["total_items"] = len(products) # Создаем экземпляр переводчика translator = ProductTranslator() # Переводим товары translated_products = [] for i, product in enumerate(products): translated_product = translator.translate_product(product) translated_products.append(translated_product) translation_status["processed_items"] = i + 1 # Сохраняем переведенные данные в отдельную директорию output_filename = filename.replace( "_products.json", "_translated_products.json" ) with open( os.path.join("output/translated", output_filename), "w", encoding="utf-8" ) as f: json.dump(translated_products, f, ensure_ascii=False, indent=2) except Exception as e: translation_status["error"] = str(e) print(f"Ошибка перевода: {e}") finally: translation_status["is_running"] = False @app.route("/update-settings", methods=["POST"]) def update_settings(): """Обновление настроек приложения""" try: data = request.json if "items_limit" in data: items_limit = int(data["items_limit"]) if items_limit == -1 or items_limit >= 1: app_settings["items_limit"] = items_limit return jsonify({"success": True}) else: return jsonify({"error": "Значение должно быть -1 или больше 0"}) except Exception as e: return jsonify({"error": str(e)}) @app.route("/generate-yml", methods=["POST"]) def generate_yml(): """Обработчик генерации YML файла""" try: data = request.get_json() print(f"Received data: {data}") filename = data.get("filename") category_id = data.get("category_id") if not filename or not category_id: return jsonify({"error": "Не вказано файл або категорію"}) # Загружаем категории categories = load_categories() category = next((c for c in categories if str(c["id"]) == str(category_id)), None) if not category: return jsonify({"error": "Категорія не знайдена"}) portal_category_id = category.get("portal_id") if not portal_category_id: return jsonify({"error": "Категорія не має portal_id (ідентифікатор категорії Prom.ua)"}) os.makedirs("output/yml", exist_ok=True) # Читаем JSON файл с переведенными товарами input_path = os.path.join("output/translated", filename) if not os.path.exists(input_path): return jsonify({"error": "Файл з товарами не знайдено"}) with open(input_path, "r", encoding="utf-8") as f: products = json.load(f) # Присваиваем portal_category_id всем товарам for product in products: product["portal_category_id"] = portal_category_id product["local_category_id"] = category["id"] # Создаем генератор YML с указанием базового URL generator = RobotVacuumYMLGenerator(base_url=BASE_URL) generator.add_category(str(category["id"]), category["name"]) # Генерируем имя выходного файла output_filename = filename.replace("_translated_products.json", ".yml") output_path = os.path.join("output/yml", output_filename) # Генерируем YML файл result = generator.generate_yml(products, output_path) if result: return jsonify({"success": True}) else: return jsonify({"error": "Помилка при генерації YML файлу"}) except Exception as e: print(f"Error generating YML: {str(e)}") return jsonify({"error": str(e)}) @app.route("/add-category", methods=["POST"]) def add_category(): """Додавання нової категорії (локальної + portal_id)""" try: data = request.json categories = load_categories() # Перевірка обов'язкових полів if "id" not in data or "name" not in data: return jsonify({"error": "Обов'язкові поля: id, name"}) # Перевірка унікальності ID if any(str(c["id"]) == str(data["id"]) for c in categories): return jsonify({"error": "Категорія з таким ID вже існує"}) # Додаємо категорію з optional portal_id new_category = { "id": data["id"], "name": data["name"], } if "portal_id" in data: new_category["portal_id"] = data["portal_id"] categories.append(new_category) save_categories(categories) return jsonify({"success": True}) except Exception as e: return jsonify({"error": str(e)}) @app.route("/delete-category", methods=["POST"]) def delete_category(): """Удаление категории""" try: data = request.json categories = load_categories() categories = [c for c in categories if c["id"] != data["id"]] save_categories(categories) return jsonify({"success": True}) except Exception as e: return jsonify({"error": str(e)}) @app.route("/get-yml-files") def get_yml_files(): """Получение списка YML файлов""" yml_files = [] if os.path.exists("output/yml"): files = [f for f in os.listdir("output/yml") if f.endswith(".yml")] yml_files = [get_file_info(f, "output/yml") for f in files] return jsonify(yml_files) @app.route("/get-translated-files") def get_translated_files(): """Получение списка переведенных файлов""" translated_files = [] if os.path.exists("output/translated"): files = [ f for f in os.listdir("output/translated") if f.endswith("_translated_products.json") ] translated_files = [get_file_info(f, "output/translated") for f in files] return jsonify(translated_files) @app.route("/get-parsed-files") def get_parsed_files(): """Получение списа спарсенных файлов""" parsed_files = [] if os.path.exists("output"): files = [ f for f in os.listdir("output") if f.endswith("_products.json") and not f.endswith("_translated_products.json") ] parsed_files = [get_file_info(f, "output") for f in files] return jsonify(parsed_files) @app.errorhandler(404) def not_found_error(error): return jsonify({"error": "Файл не найден"}), 404 @app.errorhandler(500) def internal_error(error): return jsonify({"error": "Внутренняя ошибка сервера"}), 500 @app.route("/get-files/") def get_files(file_type): """Получение списка файлов""" files = [] if file_type == "parsed": directory = "output" pattern = lambda f: f.endswith("_products.json") and not f.endswith( "_translated_products.json" ) elif file_type == "translated": directory = "output/translated" pattern = lambda f: f.endswith("_translated_products.json") elif file_type == "yml": directory = "output/yml" pattern = lambda f: f.endswith(".yml") else: return jsonify([]) if os.path.exists(directory): files = [f for f in os.listdir(directory) if pattern(f)] files = [get_file_info(f, directory) for f in files] return jsonify(files) # Добавляем роуты для отдачи изображений @app.route("/images/products/") def serve_product_image(filename): """Отдача изображений товаров""" return send_from_directory("images/products", filename) @app.route("/images/descriptions/") def serve_description_image(filename): """Отдача изображений описаний""" return send_from_directory("images/descriptions", filename) # Добавляем функцию для получения полного URL изображения def get_image_url(local_path: str) -> str: """Преобразует локальный путь в полный URL""" if not local_path: return None return urljoin(BASE_URL, local_path) def get_file_info(filename, directory): """Получение информации о файле""" path = os.path.join(directory, filename) stat = os.stat(path) return { "name": filename, "modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"), "size": f"{stat.st_size / 1024:.1f} KB", } if __name__ == "__main__": # Создаем необходимые директории for directory in ["output", "output/translated", "output/yml"]: os.makedirs(directory, exist_ok=True) # Создаем файл категорий, если его нет if not os.path.exists(CATEGORIES_FILE): save_categories([]) app.run(host='0.0.0.0', port=5000, debug=True)