diff --git a/categories.json b/categories.json index d80e027..4aaae61 100644 --- a/categories.json +++ b/categories.json @@ -1,7 +1,68 @@ [ { - "id": "1", - "name": "Роботи-пилососи", - "portal_id": "63023" + "id": "2", + "name": "Монітори", + "portal_id": "706", + "url": "https://www.euro.com.pl/monitory-led-i-lcd.bhtml" + }, + { + "id": "3", + "name": "Саундбар", + "portal_id": "518", + "url": "https://www.euro.com.pl/soundbar-speakerbar.bhtml" + }, + { + "id": "4", + "name": "Процесори", + "portal_id": "70701", + "url": "https://www.euro.com.pl/procesory.bhtml" + }, + { + "id": "5", + "name": "Навушники", + "portal_id": "63715", + "url": "https://www.euro.com.pl/sluchawki.bhtml" + }, + { + "id": "6", + "name": "Колонки", + "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 Годинники", + "portal_id": "721", + "url": "https://www.euro.com.pl/smartwatch.bhtml" + }, + { + "id": "9", + "name": "Відеокарти", + "portal_id": "70703", + "url": "https://www.euro.com.pl/karty-graficzne.bhtml" + }, + { + "id": "10", + "name": "Проектори", + "portal_id": "6370701", + "url": "https://www.euro.com.pl/projektory-multimedialne.bhtml" + }, + { + "id": "11", + "name": "Сервери", + "portal_id": "70302", + "url": "https://www.euro.com.pl/chmura-osobista-dyski-sieciowe1.bhtml" + }, + { + "id": "12", + "name": "Роботи пилососи", + "portal_id": "63023", + "url": "https://www.euro.com.pl/odkurzacze-automatyczne.bhtml" } ] \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..f8201d5 --- /dev/null +++ b/config.py @@ -0,0 +1 @@ +BASE_URL = "https://mario.mrakells.com" diff --git a/feed_generator.py b/feed_generator.py index 7a975d3..22d2ff1 100644 --- a/feed_generator.py +++ b/feed_generator.py @@ -1,3 +1,5 @@ +from config import BASE_URL + import json import xml.etree.ElementTree as ET from typing import List, Dict @@ -9,8 +11,9 @@ class RobotVacuumYMLGenerator: def __init__( self, shop_name: str = "Euro Electronics", - base_url: str = "https://mario.mrakells.pp.ua", + base_url: str = BASE_URL, use_original_urls: bool = False, + categories_data: List[Dict] = None, ): """ Initialize YML feed generator @@ -29,6 +32,7 @@ class RobotVacuumYMLGenerator: self.categories = ET.SubElement(self.shop, "categories") self.offers = ET.SubElement(self.shop, "offers") + self.categories_data = categories_data or [] def add_category(self, category_id: str, category_name: str, parent_id: str = None): """ @@ -110,13 +114,17 @@ class RobotVacuumYMLGenerator: :param product: Product dictionary from JSON """ - in_stock = product.get('in_stock', False) + in_stock = product.get("in_stock", False) - offer = ET.SubElement(self.offers, 'offer', { - 'id': str(product['plu']), - 'available': 'true' if in_stock else 'false', - 'in_stock': 'true' if in_stock else 'false' - }) + offer = ET.SubElement( + self.offers, + "offer", + { + "id": str(product["plu"]), + "available": "true" if in_stock else "false", + "in_stock": "true" if in_stock else "false", + }, + ) # Clean product name before adding to feed cleaned_name = self.clean_product_name(product["name"]) @@ -134,6 +142,28 @@ class RobotVacuumYMLGenerator: product["portal_category_id"] ) # ОБОВ'ЯЗКОВО + # Додаємо keywords із назви категорії + category_name = "" + local_category_id = product.get("local_category_id") + if self.categories_data: + match = next( + ( + c + for c in self.categories_data + if str(c["id"]) == str(local_category_id) + ), + None, + ) + if match: + category_name = match["name"] + + keywords = product.get("keywords", "") + combined_keywords = category_name + if keywords: + combined_keywords += f", {keywords}" + + ET.SubElement(offer, "keywords").text = combined_keywords + # Description with images if "description" in product: description_html = "
+ Парсинг запущено для категорії: ${data.current_category}
+ `;
+
+ if (data.total_products > 0) {
+ statusHtml += `
+ Товар ${data.current_product} із ${data.total_products}
+ Прогрес: ${data.progress}%
+ `;
+ } else {
+ statusHtml += `Отримання інформації про кількість товарів...`;
+ }
+
+ statusHtml += `
Помилка: ${data.error}
`; + } else if (data.total_products > 0) { + status.innerHTML = ` +
+ Парсинг завершено
+ Всього оброблено товарів: ${data.total_products}
+
Помилка: ${error.message}
`; + button.disabled = false; + }); +} + +function stopStatusCheck() { + if (statusCheckInterval) { + clearInterval(statusCheckInterval); + statusCheckInterval = null; + } +} + +function deleteFile(filename, type) { + if (confirm('Ви впевнені, що хочете видалити цей файл?')) { + fetch(`/delete/${filename}`, { + method: 'POST' + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + alert(data.error); + } else { + // Удаляем файл из списка + const fileItem = document.querySelector(`a[href*="${filename}"]`).closest('li'); + fileItem.remove(); + + // Обновляем соответствующий select в зависимости от типа файла + if (type === 'parsed') { + const fileSelect = document.getElementById('file-select'); + const optionToRemove = Array.from(fileSelect.options).find(opt => opt.value === filename); + if (optionToRemove) { + optionToRemove.remove(); + } + } else if (type === 'translated') { + const ymlFileSelect = document.getElementById('yml-file-select'); + const optionToRemove = Array.from(ymlFileSelect.options).find(opt => opt.value === filename); + if (optionToRemove) { + optionToRemove.remove(); + } + } + } + }) + .catch(error => { + console.error('Error:', error); + alert('Помилка при видаленні файлу'); + }); + } +} + +function openTab(tabName) { + // Приховуємо всі вкладки + const tabContents = document.getElementsByClassName('tab-content'); + for (let content of tabContents) { + content.classList.remove('active'); + } + + // Деактивуємо всі кнопки + const tabButtons = document.getElementsByClassName('tab-button'); + for (let button of tabButtons) { + button.classList.remove('active'); + } + + // Показуємо вибрану вкладку + document.getElementById(tabName).classList.add('active'); + + // Активуємо потрібну кнопку + event.currentTarget.classList.add('active'); +} + +function startTranslation() { + const fileSelect = document.getElementById('file-select'); + const button = document.getElementById('translateButton'); + const status = document.getElementById('translation-status'); + + if (!fileSelect.value) { + status.innerHTML = 'Будь ласка, виберіть файл для обробки
'; + return; + } + + button.disabled = true; + status.innerHTML = 'Починаємо переклад...
'; + + fetch('/translate', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `filename=${encodeURIComponent(fileSelect.value)}` + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + status.innerHTML = `${data.error}
`; + button.disabled = false; + } else { + checkTranslationStatus(); + } + }); +} + +function checkTranslationStatus() { + fetch('/translation-status') + .then(response => response.json()) + .then(data => { + const status = document.getElementById('translation-status'); + const button = document.getElementById('translateButton'); + + if (data.is_running) { + if (data.total_items > 0) { + const percent = Math.round((data.processed_items / data.total_items) * 100); + status.innerHTML = ` +
+ Переклад в процесі...
+ Оброблено: ${data.processed_items} з ${data.total_items}
+ Прогрес: ${percent}%
+
Підготовка до перекладу...
'; + } + setTimeout(checkTranslationStatus, 1000); + } else { + if (data.error) { + status.innerHTML = `Помилка: ${data.error}
`; + } else if (data.total_items > 0) { + status.innerHTML = ` +
+ Переклад завершено
+ Всього оброблено товарів: ${data.total_items}
+
Помилка: ${error.message}
`; + document.getElementById('translateButton').disabled = false; + }); +} + +function generateFullYML() { + if (!confirm("Згенерувати загальний YML-файл для всіх категорій?")) return; + + fetch('/generate-full-yml', { method: 'POST' }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert("Повний YML успішно згенеровано"); + updateFilesList('yml', 'generator'); + } else { + alert("Помилка: " + (data.error || "Невідома помилка")); + } + }); +} + +function updateItemsLimit(value) { + fetch('/update-settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + items_limit: parseInt(value) + }) + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + alert(data.error); + } + }); +} + +function addCategory() { + const categoryName = document.getElementById('category-name').value; + const portalId = document.getElementById('portal-id').value; + const categoryUrl = document.getElementById('category-url').value; + + if (!categoryName || !categoryUrl) { + alert('Будь ласка, заповніть назву і URL категорії'); + return; + } + + fetch('/add-category', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: categoryName, + portal_id: portalId, + url: categoryUrl + }) + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + alert(data.error); + } else { + const newId = data.id; + + document.getElementById('category-name').value = ''; + document.getElementById('portal-id').value = ''; + document.getElementById('category-url').value = ''; + + const categoriesList = document.getElementById('categories-list'); + const li = document.createElement('li'); + li.title = categoryUrl; + li.innerHTML = ` + ${newId} - ${categoryName} ${portalId ? `(portal_id: ${portalId})` : ''} + + `; + categoriesList.appendChild(li); + + const ymlSelect = document.getElementById('yml-category-select'); + const option = document.createElement('option'); + option.value = newId; + option.text = categoryName; + ymlSelect.appendChild(option); + } + }); +} + +function deleteCategory(categoryId) { + if (confirm('Ви впевнені, що хочете видалити цю категорію?')) { + fetch('/delete-category', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: categoryId + }) + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + alert(data.error); + } else { + // Удаляем категорию из списка + const categoryItems = document.querySelectorAll('#categories-list li'); + categoryItems.forEach(item => { + if (item.querySelector('span').textContent.startsWith(categoryId + ' -')) { + item.remove(); + } + }); + + // Удаляем опцию из select в генераторе YML + const ymlSelect = document.getElementById('yml-category-select'); + const option = Array.from(ymlSelect.options).find(opt => opt.value === categoryId); + if (option) option.remove(); + } + }); + } +} + +function generateYML() { + console.log('generateYML called'); // Отладка + const categoryId = document.getElementById('yml-category-select').value; + const fileSelect = document.getElementById('yml-file-select'); + console.log('Selected category:', categoryId); // Отладка + console.log('Selected file:', fileSelect.value); // Отладка + const button = document.getElementById('generateButton'); + const status = document.getElementById('yml-status'); + + if (!categoryId || !fileSelect.value) { + status.innerHTML = 'Будь ласка, виберіть категорію та файл
'; + return; + } + + button.disabled = true; + status.innerHTML = 'Генерація YML...
'; + + fetch('/generate-yml', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + filename: fileSelect.value, + category_id: categoryId + }) + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + status.innerHTML = `${data.error}
`; + } else { + status.innerHTML = 'YML файл успішно згенеровано
'; + updateFilesList('yml', 'generator', null); + } + button.disabled = false; + }) + .catch(error => { + status.innerHTML = `Помилка: ${error.message}
`; + button.disabled = false; + }); +} + +// Добавляем вспомогательную функцию для поиска по тексту +jQuery.expr[':'].contains = function (a, i, m) { + return jQuery(a).text().toUpperCase() + .indexOf(m[3].toUpperCase()) >= 0; +}; + +function updateFilesList(fileType, containerId, selectId = null) { + fetch(`/get-files/${fileType}`) + .then(response => response.json()) + .then(files => { + // Обновляем список файлов + const filesList = document.querySelector(`#${containerId} .files ul`); + filesList.innerHTML = files.map(file => ` +Будь ласка, введіть URL
'; + return; + } + + button.disabled = true; + status.innerHTML = 'Починаємо парсинг...
'; + + fetch('/parse', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `url=${encodeURIComponent(url)}` + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + status.innerHTML = `${data.error}
`; + button.disabled = false; + } else { + checkParsingStatus(); + } + }) + .catch(error => { + status.innerHTML = `Помилка: ${error.message}
`; + button.disabled = false; + }); +} + +function checkParsingStatus() { + fetch('/status') + .then(response => response.json()) + .then(data => { + const status = document.getElementById('status'); + const button = document.getElementById('parseButton'); + + if (data.is_running) { + if (data.total_items > 0) { + const percent = Math.round((data.processed_items / data.total_items) * 100); + status.innerHTML = ` +
+ Парсинг в процесі...
+ Оброблено: ${data.processed_items} з ${data.total_items}
+ Прогрес: ${percent}%
+
Отримання інформації про товари...
'; + } + setTimeout(checkParsingStatus, 1000); + } else { + if (data.error) { + status.innerHTML = `Помилка: ${data.error}
`; + } else { + status.innerHTML = ` +
+ Парсинг завершено
+ Всього оброблено товарів: ${data.total_items}
+
Помилка: ${error.message}
`; + document.getElementById('parseButton').disabled = false; + }); +} + +function refreshOldestCategory() { + fetch('/manual-refresh-all', { + method: 'POST' + }) +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 51ac82f..10ea1e5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,393 +3,17 @@🕓 Фід оновлено: {{ feed_file_info.modified }}
+ {% endif %} + +