big changes, sorry

This commit is contained in:
2025-05-27 12:22:52 +03:00
parent bb12fe5ca7
commit bcee61a817
7 changed files with 371 additions and 98 deletions

View File

@@ -29,12 +29,6 @@
"portal_id": "518", "portal_id": "518",
"url": "https://www.euro.com.pl/glosniki-komputerowe.bhtml" "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", "id": "8",
"name": "Smart Watch Годинники", "name": "Smart Watch Годинники",
@@ -64,5 +58,275 @@
"name": "Роботи пилососи", "name": "Роботи пилососи",
"portal_id": "63023", "portal_id": "63023",
"url": "https://www.euro.com.pl/odkurzacze-automatyczne.bhtml" "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"
} }
] ]

View File

@@ -357,7 +357,8 @@ def extract_product_info(
"shop": shop_delivery, "shop": shop_delivery,
"home": home_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 driver, # Передаем driver в clean_description
filtered_product["plu"], filtered_product["plu"],
) )
all_products.append(filtered_product) if filtered_product["prices"]["mainPrice"] >= 300:
all_products.append(filtered_product)
current_product += 1 current_product += 1
if status is not None: if status is not None:

View File

@@ -214,11 +214,16 @@ class RobotVacuumYMLGenerator:
if not list(self.categories): if not list(self.categories):
raise ValueError("No categories added to the YML feed.") raise ValueError("No categories added to the YML feed.")
# Add offers for each product # ✅ Фильтрация товаров дороже 300 злотых
for product in products: filtered_products = [
product
for product in products
if product.get("prices", {}).get("mainPrice", 0) > 300
]
for product in filtered_products:
self.add_offer(product) self.add_offer(product)
# Write the XML tree
tree = ET.ElementTree(self.root) tree = ET.ElementTree(self.root)
tree.write(output_yml, encoding="UTF-8", xml_declaration=True) tree.write(output_yml, encoding="UTF-8", xml_declaration=True)
print(f"YML feed generated: {output_yml}") print(f"YML feed generated: {output_yml}")

View File

@@ -261,7 +261,7 @@ button:disabled {
border-color: var(--blue); border-color: var(--blue);
} }
.items-limit-group { .items-button-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@@ -269,25 +269,6 @@ button:disabled {
height: 44px; 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 { .categories-section {
margin-bottom: 2rem; margin-bottom: 2rem;
background: var(--surface0); background: var(--surface0);

View File

@@ -13,25 +13,19 @@
<p>🕓 <a href="{{ url_for('serve_feed') }}" target="_blank">Фід</a> оновлено: {{ feed_file_info.modified }}</p> <p>🕓 <a href="{{ url_for('serve_feed') }}" target="_blank">Фід</a> оновлено: {{ feed_file_info.modified }}</p>
{% endif %} {% endif %}
<div class="tabs"> <div class="tabs">
<div class="tab-buttons"> <div class="tab-buttons">
<button class="tab-button active" onclick="openTab('parser')">Парсер</button> <button class="tab-button active" onclick="openTab('parser')">Парсер</button>
<button class="tab-button" onclick="openTab('processor')">Переклад</button> <button class="tab-button" onclick="openTab('processor')">Переклад</button>
<button class="tab-button" onclick="openTab('generator')">YML Фід</button> <button class="tab-button" onclick="openTab('generator')">YML Фід</button>
<button class="tab-button" onclick="openTab('categories')">Категорії</button> <button class="tab-button" onclick="openTab('categories')">Категорії</button>
<div class="items-limit-group"> <div class="items-button-group">
<label for="items-limit">Ліміт:</label> <button onclick="refreshOldestCategory()">🔄</button>
<input type="number" id="items-limit" value="{{ app_settings.items_limit }}" min="-1" <button onclick="translateAllCategories()">🇺🇦</button>
onchange="updateItemsLimit(this.value)" title="Встановіть -1 для зняття обмежень"> <button onclick="generateFullYML()">YML</button>
</div> </div>
<button onclick="refreshOldestCategory()">🔄</button>
<button onclick="translateAllCategories()">🇺🇦</button>
<button onclick="generateFullYML()">YML</button>
</div> </div>
<!-- Вкладка парсера --> <!-- Вкладка парсера -->
<div id="parser" class="tab-content active"> <div id="parser" class="tab-content active">
<div class="form-group"> <div class="form-group">

View File

@@ -2,15 +2,16 @@ from deep_translator import GoogleTranslator
from typing import Dict, Any, List from typing import Dict, Any, List
import time import time
class ProductTranslator: class ProductTranslator:
def __init__(self): def __init__(self):
self.translator = GoogleTranslator(source='pl', target='uk') self.translator = GoogleTranslator(source="pl", target="uk")
def translate_text(self, text: str) -> str: def translate_text(self, text: str) -> str:
"""Переводит текст с обработкой ошибок и задержкой""" """Переводит текст с обработкой ошибок и задержкой"""
if not text or not isinstance(text, str): if not text or not isinstance(text, str):
return text return text
try: try:
translated = self.translator.translate(text) translated = self.translator.translate(text)
time.sleep(0.5) # Задержка чтобы избежать блокировки time.sleep(0.5) # Задержка чтобы избежать блокировки
@@ -18,27 +19,55 @@ class ProductTranslator:
except Exception as e: except Exception as e:
print(f"Ошибка перевода: {e}") print(f"Ошибка перевода: {e}")
return text return text
def translate_list(self, items: List[str]) -> List[str]: def translate_list(self, items: List[str]) -> List[str]:
"""Переводит список строк""" """Переводит список строк"""
return [self.translate_text(item) for item in items] 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 = product.copy()
any_changes = False
# Переводим название
translated['name'] = self.translate_text(product['name']) # Название
translated_name = self.translate_text(product["name"])
# Переводим атрибуты if self.is_translated(product["name"], translated_name):
for attr in translated['attributes']: translated["name"] = translated_name
attr['name'] = self.translate_text(attr['name']) any_changes = True
attr['value'] = self.translate_list(attr['value']) else:
print(f"[SKIP] Название не переведено: {product['name']}")
# Переводим описание
if 'description' in translated: # Атрибути
for section in translated['description']: if "attributes" in translated:
section['title'] = self.translate_text(section['title']) for attr in translated["attributes"]:
section['text'] = self.translate_text(section['text']) name_trans = self.translate_text(attr["name"])
if self.is_translated(attr["name"], name_trans):
return translated 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

View File

@@ -474,9 +474,6 @@ def start_translation(filename: str):
product["portal_category_id"] = category["portal_id"] product["portal_category_id"] = category["portal_id"]
product["local_category_id"] = category["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) translation_status["total_items"] = len(products)
# Создаем экземпляр переводчика # Создаем экземпляр переводчика
@@ -486,17 +483,24 @@ def start_translation(filename: str):
translated_products = [] translated_products = []
for i, product in enumerate(products): for i, product in enumerate(products):
translated_product = translator.translate_product(product) 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 translation_status["processed_items"] = i + 1
# Сохраняем переведенные данные в отдельную директорию # Сохраняем переведенные данные в отдельную директорию
output_filename = filename.replace( if translated_products:
"_products.json", "_translated_products.json" output_filename = filename.replace(
) "_products.json", "_translated_products.json"
with open( )
os.path.join("output/translated", output_filename), "w", encoding="utf-8" with open(
) as f: os.path.join("output/translated", output_filename),
json.dump(translated_products, f, ensure_ascii=False, indent=2) "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: except Exception as e:
translation_status["error"] = str(e) translation_status["error"] = str(e)
@@ -546,6 +550,7 @@ def refresh_all_categories_daily():
parsing_status["is_running"] = False parsing_status["is_running"] = False
time.sleep(5) time.sleep(5)
generate_full_yml()
print("[DONE] Автоматичне оновлення завершено") print("[DONE] Автоматичне оновлення завершено")
@@ -577,22 +582,6 @@ def translate_all_parsed_once():
print("[DONE] Запуск перекладу завершено.") 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"]) @app.route("/manual-translate-all", methods=["POST"])
@login_required @login_required
def manual_translate_all(): def manual_translate_all():
@@ -617,14 +606,23 @@ def generate_full_yml():
if not products: if not products:
continue continue
# Перевірка на наявність ID # Витягуємо slug із назви файлу
if not all( slug = file.replace("_translated_products.json", "")
"portal_category_id" in p and "local_category_id" in p
for p in products # Шукаємо відповідну категорію за slug у URL
): category_match = next((c for c in categories if slug in c["url"]), None)
print(f"[SKIP] {file} — відсутні ID")
if not category_match:
print(f"[SKIP] {file} — не знайдено категорію для slug: {slug}")
continue 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) all_products.extend(products)
if not all_products: if not all_products: