diff --git a/categories.json b/categories.json index 4aaae61..b28ef0b 100644 --- a/categories.json +++ b/categories.json @@ -29,12 +29,6 @@ "portal_id": "518", "url": "https://www.euro.com.pl/glosniki-komputerowe.bhtml" }, - { - "id": "7", - "name": "Ноутбуки", - "portal_id": "702", - "url": "https://www.euro.com.pl/laptopy-i-netbooki.bhtml" - }, { "id": "8", "name": "Smart Watch Годинники", @@ -64,5 +58,275 @@ "name": "Роботи пилососи", "portal_id": "63023", "url": "https://www.euro.com.pl/odkurzacze-automatyczne.bhtml" + }, + { + "id": "13", + "name": "Кавоварки", + "portal_id": "638", + "url": "https://www.euro.com.pl/ekspresy-cisnieniowe.bhtml" + }, + { + "id": "14", + "name": "Фритюрниці", + "portal_id": "64306", + "url": "https://www.euro.com.pl/frytkownice.bhtml" + }, + { + "id": "15", + "name": "Ігрові приставки", + "portal_id": "70801", + "url": "https://www.euro.com.pl/kierownice-pc,strona-2.bhtml" + }, + { + "id": "16", + "name": "Ігрові крісла", + "portal_id": "15030309", + "url": "https://www.euro.com.pl/fotele-i-stojaki-do-kierownicy.bhtml" + }, + { + "id": "17", + "name": " Вертикальні пилососи", + "portal_id": "63021", + "url": "https://www.euro.com.pl/odkurzacze-pionowe.bhtml" + }, + { + "id": "18", + "name": " Мінімийки", + "portal_id": "121105", + "url": "https://www.euro.com.pl/myjki-wysokocisnieniowe.bhtml" + }, + { + "id": "19", + "name": " Робот-косарка", + "portal_id": "1250308", + "url": "https://www.euro.com.pl/roboty-koszace.bhtml" + }, + { + "id": "20", + "name": " Сантехніка", + "portal_id": "130103", + "url": "https://www.euro.com.pl/baterie-kuchenne.bhtml" + }, + { + "id": "21", + "name": "Кухонні мийки", + "portal_id": "1302", + "url": "https://www.euro.com.pl/zestawy-armatury-kuchennej.bhtml" + }, + { + "id": "22", + "name": "Витяжки", + "portal_id": "642", + "url": "https://www.euro.com.pl/okapy.bhtml" + }, + { + "id": "23", + "name": "Парогенератори", + "portal_id": "147008", + "url": "https://www.euro.com.pl/zelazka-systemowe,strona-2.bhtml" + }, + { + "id": "24", + "name": "Вбудовані варочні поверхні", + "portal_id": "61911", + "url": "https://www.euro.com.pl/plyty-do-zabudowy.bhtml" + }, + { + "id": "25", + "name": "Духовки", + "portal_id": "61903", + "url": "https://www.euro.com.pl/piekarniki-do-zabudowy.bhtml" + }, + { + "id": "26", + "name": " Вбудовані мікрохвильові печі", + "portal_id": "605", + "url": "https://www.euro.com.pl/kuchenki-mikrofalowe-do-zabudowy.bhtml" + }, + { + "id": "27", + "name": "Грилі", + "portal_id": "63009", + "url": "https://www.euro.com.pl/grille,strona-4.bhtml" + }, + { + "id": "28", + "name": " Електрочайники", + "portal_id": "640", + "url": "https://www.euro.com.pl/czajniki,strona-2.bhtml" + }, + { + "id": "29", + "name": "Соковижималки (соковичавниці)", + "portal_id": "611", + "url": "https://www.euro.com.pl/wyciskarki-wolnoobrotowe.bhtml" + }, + { + "id": "30", + "name": "Блендери", + "portal_id": "64301", + "url": "https://www.euro.com.pl/miksery-kielichowe.bhtml" + }, + { + "id": "31", + "name": " Тостери", + "portal_id": "639", + "url": "https://www.euro.com.pl/tostery.bhtml" + }, + { + "id": "32", + "name": "Електробритви", + "portal_id": "3314", + "url": "https://www.euro.com.pl/golarki.bhtml" + }, + { + "id": "33", + "name": "Зубні щітки та іригатори", + "portal_id": "63301", + "url": "https://www.euro.com.pl/szczoteczki-elektryczne.bhtml" + }, + { + "id": "35", + "name": " Плойки", + "portal_id": "63303", + "url": "https://www.euro.com.pl/prostownice-i-karbownice.bhtml" + }, + { + "id": "37", + "name": "Графічні планшети", + "portal_id": "70804", + "url": "https://www.euro.com.pl/tablety-graficzne.bhtml" + }, + { + "id": "39", + "name": "Акустична система", + "portal_id": "63701", + "url": "https://www.euro.com.pl/wzmacniacze.bhtml" + }, + { + "id": "40", + "name": "Програвачі вінілових дисків", + "portal_id": "6371201", + "url": "https://www.euro.com.pl/gramofony.bhtml" + }, + { + "id": "41", + "name": "Портативні колонки", + "portal_id": "6371101", + "url": "https://www.euro.com.pl/power-audio.bhtml" + }, + { + "id": "42", + "name": "Планшети", + "portal_id": "70105", + "url": "https://www.euro.com.pl/ipad-i-tablety-multimedialne.bhtml" + }, + { + "id": "43", + "name": "Прасувальні дошки", + "portal_id": "151602", + "url": "https://www.euro.com.pl/systemy-do-prasowania.bhtml" + }, + { + "id": "44", + "name": "Кухонні ножі", + "portal_id": "15230410", + "url": "https://www.euro.com.pl/noze-kuchenne.bhtml" + }, + { + "id": "45", + "name": " Кавомолки", + "portal_id": "638", + "url": "https://www.euro.com.pl/mlynki-do-kawy.bhtml" + }, + { + "id": "46", + "name": "Фені", + "portal_id": " 617", + "url": "https://www.euro.com.pl/suszarki-do-wlosow.bhtml" + }, + { + "id": "47", + "name": "Електросамокати", + "portal_id": "205302", + "url": "https://www.euro.com.pl/hulajnogi-elektryczne.bhtml" + }, + { + "id": "48", + "name": "Очишчувачі повітря", + "portal_id": "625", + "url": "https://www.euro.com.pl/oczyszczacze-powietrza.bhtml" + }, + { + "id": "49", + "name": " Екшн-камери", + "portal_id": "1902", + "url": "https://www.euro.com.pl/kamery-sportowe.bhtml" + }, + { + "id": "50", + "name": "Фотоапарати", + "portal_id": "1909", + "url": "https://www.euro.com.pl/kompakty-z-wymienna-optyka.bhtml" + }, + { + "id": "51", + "name": "Фотоапарати", + "portal_id": "1909", + "url": "https://www.euro.com.pl/aparaty-cyfrowe.bhtml" + }, + { + "id": "52", + "name": "Очишчувачі повітря", + "portal_id": "625", + "url": "https://www.euro.com.pl/nawilzacze,strona-2.bhtml" + }, + { + "id": "53", + "name": "Мікрофони", + "portal_id": "6371204", + "url": "https://www.euro.com.pl/mikrofony.bhtml" + }, + { + "id": "54", + "name": "Миші", + "portal_id": "70805", + "url": "https://www.euro.com.pl/myszy,strona-2.bhtml" + }, + { + "id": "55", + "name": "Принтери", + "portal_id": "70812", + "url": "https://www.euro.com.pl/urzadzenia-wielofunkcyjne.bhtml" + }, + { + "id": "56", + "name": "Принтери", + "portal_id": "70812", + "url": "https://www.euro.com.pl/drukarki-atramentowe.bhtml" + }, + { + "id": "58", + "name": "SSD накопичувачі", + "portal_id": "70704", + "url": "https://www.euro.com.pl/dyski-wewnetrzne-ssd.bhtml" + }, + { + "id": "59", + "name": "Модулі пам'яті", + "portal_id": "70705", + "url": "https://www.euro.com.pl/pamieci-ram.bhtml" + }, + { + "id": "60", + "name": "SSD накопичувачі", + "portal_id": "70704", + "url": "https://www.euro.com.pl/archiwizacja-danych1.bhtml" + }, + { + "id": "61", + "name": "Корпуси для комп'ютерів", + "portal_id": "720", + "url": "https://www.euro.com.pl/obudowy-pc.bhtml" } -] \ No newline at end of file +] diff --git a/euro_scraper.py b/euro_scraper.py index 669438b..fa3b915 100644 --- a/euro_scraper.py +++ b/euro_scraper.py @@ -357,7 +357,8 @@ def extract_product_info( "shop": shop_delivery, "home": home_delivery, }, - "in_stock": shop_delivery in ("FOR_TOMORROW", "IMMEDIATE") or home_delivery in ("FOR_TOMORROW", "IMMEDIATE"), + # "in_stock": shop_delivery in ("FOR_TOMORROW", "IMMEDIATE") or home_delivery in ("FOR_TOMORROW", "IMMEDIATE"), + "in_stock": all(code not in ("UNAVAILABLE", "TEMPORARILY_UNAVAILABLE", None) for code in [shop_delivery, home_delivery]) } @@ -459,7 +460,8 @@ def fetch_products( driver, # Передаем driver в clean_description filtered_product["plu"], ) - all_products.append(filtered_product) + if filtered_product["prices"]["mainPrice"] >= 300: + all_products.append(filtered_product) current_product += 1 if status is not None: diff --git a/feed_generator.py b/feed_generator.py index 22d2ff1..27328e1 100644 --- a/feed_generator.py +++ b/feed_generator.py @@ -214,11 +214,16 @@ class RobotVacuumYMLGenerator: if not list(self.categories): raise ValueError("No categories added to the YML feed.") - # Add offers for each product - for product in products: + # ✅ Фильтрация товаров дороже 300 злотых + filtered_products = [ + product + for product in products + if product.get("prices", {}).get("mainPrice", 0) > 300 + ] + + for product in filtered_products: self.add_offer(product) - # Write the XML tree tree = ET.ElementTree(self.root) tree.write(output_yml, encoding="UTF-8", xml_declaration=True) print(f"YML feed generated: {output_yml}") diff --git a/static/css/styles.css b/static/css/styles.css index dc868eb..065765f 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -261,7 +261,7 @@ button:disabled { border-color: var(--blue); } -.items-limit-group { +.items-button-group { display: flex; align-items: center; gap: 0.5rem; @@ -269,25 +269,6 @@ button:disabled { height: 44px; } -.items-limit-group label { - color: var(--subtext0); - font-size: 14px; -} - -#items-limit { - margin: 0; - text-align: center; - max-width: 4rem; -} - -.items-limit-group input[type="number"] { - height: 44px; - width: 4rem; - padding: 0 0.75rem; - flex-shrink: 0; - text-align: center; -} - .categories-section { margin-bottom: 2rem; background: var(--surface0); diff --git a/templates/index.html b/templates/index.html index 10ea1e5..539f116 100644 --- a/templates/index.html +++ b/templates/index.html @@ -13,25 +13,19 @@

🕓 Фід оновлено: {{ feed_file_info.modified }}

{% endif %} -
-
- - +
+ + +
- - -
- -
diff --git a/translator.py b/translator.py index 8d64db6..9b549b0 100644 --- a/translator.py +++ b/translator.py @@ -2,15 +2,16 @@ from deep_translator import GoogleTranslator from typing import Dict, Any, List import time + class ProductTranslator: def __init__(self): - self.translator = GoogleTranslator(source='pl', target='uk') - + self.translator = GoogleTranslator(source="pl", target="uk") + def translate_text(self, text: str) -> str: """Переводит текст с обработкой ошибок и задержкой""" if not text or not isinstance(text, str): return text - + try: translated = self.translator.translate(text) time.sleep(0.5) # Задержка чтобы избежать блокировки @@ -18,27 +19,55 @@ class ProductTranslator: except Exception as e: print(f"Ошибка перевода: {e}") return text - + def translate_list(self, items: List[str]) -> List[str]: """Переводит список строк""" return [self.translate_text(item) for item in items] - - def translate_product(self, product: Dict[str, Any]) -> Dict[str, Any]: - """Переводит все текстовые поля продукта""" + + def is_translated(self, original: str, translated: str) -> bool: + """Проверяет, был ли текст действительно переведен""" + return original.strip() != translated.strip() + + def translate_product(self, product: Dict[str, Any]) -> Dict[str, Any] | None: translated = product.copy() - - # Переводим название - translated['name'] = self.translate_text(product['name']) - - # Переводим атрибуты - for attr in translated['attributes']: - attr['name'] = self.translate_text(attr['name']) - attr['value'] = self.translate_list(attr['value']) - - # Переводим описание - if 'description' in translated: - for section in translated['description']: - section['title'] = self.translate_text(section['title']) - section['text'] = self.translate_text(section['text']) - - return translated \ No newline at end of file + any_changes = False + + # Название + translated_name = self.translate_text(product["name"]) + if self.is_translated(product["name"], translated_name): + translated["name"] = translated_name + any_changes = True + else: + print(f"[SKIP] Название не переведено: {product['name']}") + + # Атрибути + if "attributes" in translated: + for attr in translated["attributes"]: + name_trans = self.translate_text(attr["name"]) + if self.is_translated(attr["name"], name_trans): + attr["name"] = name_trans + any_changes = True + + values_trans = self.translate_list(attr["value"]) + if values_trans != attr["value"]: + attr["value"] = values_trans + any_changes = True + + # Опис + if "description" in translated: + for section in translated["description"]: + title_trans = self.translate_text(section["title"]) + text_trans = self.translate_text(section["text"]) + + if self.is_translated(section["title"], title_trans): + section["title"] = title_trans + any_changes = True + if self.is_translated(section["text"], text_trans): + section["text"] = text_trans + any_changes = True + + if any_changes: + return translated + else: + print(f"[SKIP] Товар не содержит переведенных полей: {product['name']}") + return None diff --git a/web_interface.py b/web_interface.py index 9593537..54801f1 100644 --- a/web_interface.py +++ b/web_interface.py @@ -474,9 +474,6 @@ def start_translation(filename: str): product["portal_category_id"] = category["portal_id"] product["local_category_id"] = category["id"] - # Ограничиваем количество товаров только если лимит больше 0 - if app_settings["items_limit"] > 0: - products = products[: app_settings["items_limit"]] translation_status["total_items"] = len(products) # Создаем экземпляр переводчика @@ -486,17 +483,24 @@ def start_translation(filename: str): translated_products = [] for i, product in enumerate(products): translated_product = translator.translate_product(product) - translated_products.append(translated_product) + if translated_product is not None: # ✅ фільтрація + 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) + if translated_products: + 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) + print(f"[OK] Збережено переклад: {output_filename}") + else: + print(f"[SKIP] Жодного перекладеного товару: {filename}. Файл не створено.") except Exception as e: translation_status["error"] = str(e) @@ -546,6 +550,7 @@ def refresh_all_categories_daily(): parsing_status["is_running"] = False time.sleep(5) + generate_full_yml() print("[DONE] Автоматичне оновлення завершено") @@ -577,22 +582,6 @@ def translate_all_parsed_once(): print("[DONE] Запуск перекладу завершено.") -@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("/manual-translate-all", methods=["POST"]) @login_required def manual_translate_all(): @@ -617,14 +606,23 @@ def generate_full_yml(): if not products: continue - # Перевірка на наявність ID - if not all( - "portal_category_id" in p and "local_category_id" in p - for p in products - ): - print(f"[SKIP] {file} — відсутні ID") + # Витягуємо slug із назви файлу + slug = file.replace("_translated_products.json", "") + + # Шукаємо відповідну категорію за slug у URL + category_match = next((c for c in categories if slug in c["url"]), None) + + if not category_match: + print(f"[SKIP] {file} — не знайдено категорію для slug: {slug}") continue + for product in products: + product["portal_category_id"] = category_match["portal_id"] + product["local_category_id"] = category_match["id"] + + print( + f"[OK] {file} — додано категорії {category_match['id']}, {category_match['portal_id']}" + ) all_products.extend(products) if not all_products: