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