feat: auto update
This commit is contained in:
@@ -1,7 +1,68 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "2",
|
||||||
"name": "Роботи-пилососи",
|
"name": "Монітори",
|
||||||
"portal_id": "63023"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from config import BASE_URL
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
@@ -9,8 +11,9 @@ class RobotVacuumYMLGenerator:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
shop_name: str = "Euro Electronics",
|
shop_name: str = "Euro Electronics",
|
||||||
base_url: str = "https://mario.mrakells.pp.ua",
|
base_url: str = BASE_URL,
|
||||||
use_original_urls: bool = False,
|
use_original_urls: bool = False,
|
||||||
|
categories_data: List[Dict] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize YML feed generator
|
Initialize YML feed generator
|
||||||
@@ -29,6 +32,7 @@ class RobotVacuumYMLGenerator:
|
|||||||
|
|
||||||
self.categories = ET.SubElement(self.shop, "categories")
|
self.categories = ET.SubElement(self.shop, "categories")
|
||||||
self.offers = ET.SubElement(self.shop, "offers")
|
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):
|
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
|
: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', {
|
offer = ET.SubElement(
|
||||||
'id': str(product['plu']),
|
self.offers,
|
||||||
'available': 'true' if in_stock else 'false',
|
"offer",
|
||||||
'in_stock': 'true' if in_stock else 'false'
|
{
|
||||||
})
|
"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
|
# Clean product name before adding to feed
|
||||||
cleaned_name = self.clean_product_name(product["name"])
|
cleaned_name = self.clean_product_name(product["name"])
|
||||||
@@ -134,6 +142,28 @@ class RobotVacuumYMLGenerator:
|
|||||||
product["portal_category_id"]
|
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
|
# Description with images
|
||||||
if "description" in product:
|
if "description" in product:
|
||||||
description_html = "<div>"
|
description_html = "<div>"
|
||||||
|
|||||||
82
static/css/login.css
Normal file
82
static/css/login.css
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #1a1b26;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 100px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background-color: #282a36;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h1 {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #8be9fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #44475a;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #1a1b26;
|
||||||
|
color: #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #6272a4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #7aa2f7;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button:hover {
|
||||||
|
background-color: #6b91e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #ff5555;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
387
static/css/styles.css
Normal file
387
static/css/styles.css
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
:root {
|
||||||
|
/* Catppuccin Mocha */
|
||||||
|
--rosewater: #f5e0dc;
|
||||||
|
--flamingo: #f2cdcd;
|
||||||
|
--pink: #f5c2e7;
|
||||||
|
--mauve: #cba6f7;
|
||||||
|
--red: #f38ba8;
|
||||||
|
--maroon: #eba0ac;
|
||||||
|
--peach: #fab387;
|
||||||
|
--yellow: #f9e2af;
|
||||||
|
--green: #a6e3a1;
|
||||||
|
--teal: #94e2d5;
|
||||||
|
--sky: #89dceb;
|
||||||
|
--sapphire: #74c7ec;
|
||||||
|
--blue: #89b4fa;
|
||||||
|
--lavender: #b4befe;
|
||||||
|
--text: #cdd6f4;
|
||||||
|
--subtext1: #bac2de;
|
||||||
|
--subtext0: #a6adc8;
|
||||||
|
--overlay2: #9399b2;
|
||||||
|
--overlay1: #7f849c;
|
||||||
|
--overlay0: #6c7086;
|
||||||
|
--surface2: #585b70;
|
||||||
|
--surface1: #45475a;
|
||||||
|
--surface0: #313244;
|
||||||
|
--base: #1e1e2e;
|
||||||
|
--mantle: #181825;
|
||||||
|
--crust: #11111b;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--base);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border: 1px solid var(--surface1);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--surface0);
|
||||||
|
color: var(--text);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="number"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Убираем стрелки для input number */
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: var(--blue);
|
||||||
|
color: var(--base);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--lavender);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: var(--overlay0);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--surface1);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--surface0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.files {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files li {
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--surface1);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: var(--surface0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.files li:hover {
|
||||||
|
background-color: var(--surface1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-link {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 2rem;
|
||||||
|
margin-right: auto;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-link:hover {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-link::before {
|
||||||
|
content: "📥";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-date,
|
||||||
|
.file-size {
|
||||||
|
color: var(--subtext0);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--red);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
color: var(--maroon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стилі для вкладок */
|
||||||
|
.tabs {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 24px;
|
||||||
|
background-color: var(--surface0);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--subtext0);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button:hover {
|
||||||
|
background-color: var(--surface1);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.active {
|
||||||
|
background-color: var(--blue);
|
||||||
|
color: var(--base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
background: var(--surface0);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border: 1px solid var(--surface1);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--surface0);
|
||||||
|
color: var(--text);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23cdd6f4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.75rem center;
|
||||||
|
background-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-limit-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-left: auto;
|
||||||
|
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);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-form .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-form button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button {
|
||||||
|
background-color: var(--surface1);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-button:hover {
|
||||||
|
background-color: var(--surface2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-list ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-list li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
background: var(--surface1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yml-generation-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
background: var(--surface0);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: relative;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin: -10px 0 0 -10px;
|
||||||
|
border: 2px solid var(--blue);
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--red);
|
||||||
|
color: var(--base);
|
||||||
|
border-radius: 8px;
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
530
static/js/main.js
Normal file
530
static/js/main.js
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
let statusCheckInterval;
|
||||||
|
|
||||||
|
function startStatusCheck() {
|
||||||
|
// Очищаем предыдущий интервал, если он существует
|
||||||
|
if (statusCheckInterval) {
|
||||||
|
clearInterval(statusCheckInterval);
|
||||||
|
}
|
||||||
|
// Проверяем статус каждые 500мс
|
||||||
|
statusCheckInterval = setInterval(checkStatus, 500);
|
||||||
|
// Сразу делаем первую проверку
|
||||||
|
checkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkStatus() {
|
||||||
|
fetch('/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
const button = document.getElementById('parseButton');
|
||||||
|
|
||||||
|
if (data.is_running) {
|
||||||
|
let statusHtml = `
|
||||||
|
<p>
|
||||||
|
Парсинг запущено для категорії: ${data.current_category}<br>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (data.total_products > 0) {
|
||||||
|
statusHtml += `
|
||||||
|
<strong>Товар ${data.current_product} із ${data.total_products}</strong><br>
|
||||||
|
Прогрес: ${data.progress}%
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
statusHtml += `Отримання інформації про кількість товарів...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusHtml += `</p>`;
|
||||||
|
status.innerHTML = statusHtml;
|
||||||
|
setTimeout(checkStatus, 500);
|
||||||
|
} else {
|
||||||
|
if (data.error) {
|
||||||
|
status.innerHTML = `<p class="error">Помилка: ${data.error}</p>`;
|
||||||
|
} else if (data.total_products > 0) {
|
||||||
|
status.innerHTML = `
|
||||||
|
<p>
|
||||||
|
Парсинг завершено<br>
|
||||||
|
<strong>Всього оброблено товарів: ${data.total_products}</strong>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
updateFilesList('parsed', 'parser', 'file-select');
|
||||||
|
}
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Status check error:', error);
|
||||||
|
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||||||
|
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 = '<p class="error">Будь ласка, виберіть файл для обробки</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
status.innerHTML = '<p>Починаємо переклад...</p>';
|
||||||
|
|
||||||
|
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 = `<p class="error">${data.error}</p>`;
|
||||||
|
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 = `
|
||||||
|
<p>
|
||||||
|
Переклад в процесі...<br>
|
||||||
|
Оброблено: ${data.processed_items} з ${data.total_items}<br>
|
||||||
|
Прогрес: ${percent}%
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
status.innerHTML = '<p>Підготовка до перекладу...</p>';
|
||||||
|
}
|
||||||
|
setTimeout(checkTranslationStatus, 1000);
|
||||||
|
} else {
|
||||||
|
if (data.error) {
|
||||||
|
status.innerHTML = `<p class="error">Помилка: ${data.error}</p>`;
|
||||||
|
} else if (data.total_items > 0) {
|
||||||
|
status.innerHTML = `
|
||||||
|
<p>
|
||||||
|
Переклад завершено<br>
|
||||||
|
Всього оброблено товарів: ${data.total_items}
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
updateFilesList('translated', 'processor', 'yml-file-select');
|
||||||
|
}
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Translation status check error:', error);
|
||||||
|
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||||||
|
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 = `
|
||||||
|
<span>${newId} - ${categoryName} ${portalId ? `(portal_id: ${portalId})` : ''}</span>
|
||||||
|
<button class="delete-btn" onclick="deleteCategory('${newId}')" title="Видалити категорію">🗑️</button>
|
||||||
|
`;
|
||||||
|
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 = '<p class="error">Будь ласка, виберіть категорію та файл</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
status.innerHTML = '<p>Генерація YML...</p>';
|
||||||
|
|
||||||
|
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 = `<p class="error">${data.error}</p>`;
|
||||||
|
} else {
|
||||||
|
status.innerHTML = '<p>YML файл успішно згенеровано</p>';
|
||||||
|
updateFilesList('yml', 'generator', null);
|
||||||
|
}
|
||||||
|
button.disabled = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||||||
|
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 => `
|
||||||
|
<li>
|
||||||
|
<div class="file-info">
|
||||||
|
<a href="/download/${file.name}" class="download-link" download>
|
||||||
|
${file.name}
|
||||||
|
</a>
|
||||||
|
<span class="file-date">${file.modified}</span>
|
||||||
|
<span class="file-size">${file.size}</span>
|
||||||
|
</div>
|
||||||
|
<button class="delete-btn" onclick="deleteFile('${file.name}', '${fileType}')" title="Видалити файл">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Обновляем select, если указан
|
||||||
|
if (selectId) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
select.innerHTML = '<option value="">Виберіть файл...</option>' +
|
||||||
|
files.map(file => `
|
||||||
|
<option value="${file.name}">${file.name}</option>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoader(element) {
|
||||||
|
element.classList.add('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoader(element) {
|
||||||
|
element.classList.remove('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
const errorDiv = document.createElement('div');
|
||||||
|
errorDiv.className = 'error-message';
|
||||||
|
errorDiv.textContent = message;
|
||||||
|
document.body.appendChild(errorDiv);
|
||||||
|
setTimeout(() => errorDiv.remove(), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Использование:
|
||||||
|
async function someAction() {
|
||||||
|
const element = document.getElementById('someElement');
|
||||||
|
showLoader(element);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/some-endpoint');
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
// обработка успешного ответа
|
||||||
|
} catch (error) {
|
||||||
|
showError(error.message);
|
||||||
|
} finally {
|
||||||
|
hideLoader(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateAllCategories() {
|
||||||
|
if (!confirm("Запустити переклад для всіх категорій?")) return;
|
||||||
|
|
||||||
|
fetch('/manual-translate-all', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.message) {
|
||||||
|
alert(data.message);
|
||||||
|
} else {
|
||||||
|
alert("Переклад запущено.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert("Помилка при запуску перекладу: " + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function startParsing() {
|
||||||
|
const url = document.getElementById('url').value;
|
||||||
|
const button = document.getElementById('parseButton');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
status.innerHTML = '<p class="error">Будь ласка, введіть URL</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
status.innerHTML = '<p>Починаємо парсинг...</p>';
|
||||||
|
|
||||||
|
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 = `<p class="error">${data.error}</p>`;
|
||||||
|
button.disabled = false;
|
||||||
|
} else {
|
||||||
|
checkParsingStatus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||||||
|
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 = `
|
||||||
|
<p>
|
||||||
|
Парсинг в процесі...<br>
|
||||||
|
Оброблено: ${data.processed_items} з ${data.total_items}<br>
|
||||||
|
Прогрес: ${percent}%
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
status.innerHTML = '<p>Отримання інформації про товари...</p>';
|
||||||
|
}
|
||||||
|
setTimeout(checkParsingStatus, 1000);
|
||||||
|
} else {
|
||||||
|
if (data.error) {
|
||||||
|
status.innerHTML = `<p class="error">Помилка: ${data.error}</p>`;
|
||||||
|
} else {
|
||||||
|
status.innerHTML = `
|
||||||
|
<p>
|
||||||
|
Парсинг завершено<br>
|
||||||
|
Всього оброблено товарів: ${data.total_items}
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
updateFilesList('parsed', 'parser', 'file-select');
|
||||||
|
}
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Status check error:', error);
|
||||||
|
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||||||
|
document.getElementById('parseButton').disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshOldestCategory() {
|
||||||
|
fetch('/manual-refresh-all', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,393 +3,17 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Парсер mariotexno</title>
|
<title>Парсер mariotexno</title>
|
||||||
<style>
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
:root {
|
|
||||||
/* Catppuccin Mocha */
|
|
||||||
--rosewater: #f5e0dc;
|
|
||||||
--flamingo: #f2cdcd;
|
|
||||||
--pink: #f5c2e7;
|
|
||||||
--mauve: #cba6f7;
|
|
||||||
--red: #f38ba8;
|
|
||||||
--maroon: #eba0ac;
|
|
||||||
--peach: #fab387;
|
|
||||||
--yellow: #f9e2af;
|
|
||||||
--green: #a6e3a1;
|
|
||||||
--teal: #94e2d5;
|
|
||||||
--sky: #89dceb;
|
|
||||||
--sapphire: #74c7ec;
|
|
||||||
--blue: #89b4fa;
|
|
||||||
--lavender: #b4befe;
|
|
||||||
--text: #cdd6f4;
|
|
||||||
--subtext1: #bac2de;
|
|
||||||
--subtext0: #a6adc8;
|
|
||||||
--overlay2: #9399b2;
|
|
||||||
--overlay1: #7f849c;
|
|
||||||
--overlay0: #6c7086;
|
|
||||||
--surface2: #585b70;
|
|
||||||
--surface1: #45475a;
|
|
||||||
--surface0: #313244;
|
|
||||||
--base: #1e1e2e;
|
|
||||||
--mantle: #181825;
|
|
||||||
--crust: #11111b;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
color: var(--text);
|
|
||||||
background: var(--base);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
color: var(--text);
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"],
|
|
||||||
input[type="number"] {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
border: 1px solid var(--surface1);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: var(--surface0);
|
|
||||||
color: var(--text);
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"]:focus,
|
|
||||||
input[type="number"]:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Убираем стрелки для input number */
|
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="number"] {
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background-color: var(--blue);
|
|
||||||
color: var(--base);
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: var(--lavender);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background-color: var(--overlay0);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid var(--surface1);
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: var(--surface0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.files {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files li {
|
|
||||||
margin: 0.75rem 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid var(--surface1);
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
background: var(--surface0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.files li:hover {
|
|
||||||
background-color: var(--surface1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 1.5rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-link {
|
|
||||||
color: var(--text);
|
|
||||||
text-decoration: none;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 2rem;
|
|
||||||
margin-right: auto;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-link:hover {
|
|
||||||
color: var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-link::before {
|
|
||||||
content: "📥";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-date,
|
|
||||||
.file-size {
|
|
||||||
color: var(--subtext0);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--red);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-btn:hover {
|
|
||||||
color: var(--maroon);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стилі для вкладок */
|
|
||||||
.tabs {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button {
|
|
||||||
height: 44px;
|
|
||||||
padding: 0 24px;
|
|
||||||
background-color: var(--surface0);
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--subtext0);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button:hover {
|
|
||||||
background-color: var(--surface1);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button.active {
|
|
||||||
background-color: var(--blue);
|
|
||||||
color: var(--base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
display: none;
|
|
||||||
background: var(--surface0);
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
border: 1px solid var(--surface1);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: var(--surface0);
|
|
||||||
color: var(--text);
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23cdd6f4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 0.75rem center;
|
|
||||||
background-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-limit-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-left: auto;
|
|
||||||
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);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-form {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: flex-end;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-form .form-group {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-form button {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-button {
|
|
||||||
background-color: var(--surface1);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-button:hover {
|
|
||||||
background-color: var(--surface2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.categories-list ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.categories-list li {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
background: var(--surface1);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yml-generation-section {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
background: var(--surface0);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
margin: -10px 0 0 -10px;
|
|
||||||
border: 2px solid var(--blue);
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
padding: 1rem;
|
|
||||||
background: var(--red);
|
|
||||||
color: var(--base);
|
|
||||||
border-radius: 8px;
|
|
||||||
animation: slideIn 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from { transform: translateX(100%); }
|
|
||||||
to { transform: translateX(0); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>Парсер euro.com.pl</h1>
|
<h1>Парсер euro.com.pl</h1>
|
||||||
|
|
||||||
|
{% if feed_file_info %}
|
||||||
|
<p>🕓 <a href="{{ url_for('serve_feed') }}" target="_blank">Фід</a> оновлено: {{ feed_file_info.modified }}</p>
|
||||||
|
{% 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>
|
||||||
@@ -402,6 +26,8 @@
|
|||||||
onchange="updateItemsLimit(this.value)" title="Встановіть -1 для зняття обмежень">
|
onchange="updateItemsLimit(this.value)" title="Встановіть -1 для зняття обмежень">
|
||||||
</div>
|
</div>
|
||||||
<button onclick="refreshOldestCategory()">🔄</button>
|
<button onclick="refreshOldestCategory()">🔄</button>
|
||||||
|
<button onclick="translateAllCategories()">🇺🇦</button>
|
||||||
|
<button onclick="generateFullYML()">YML</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -535,8 +161,7 @@
|
|||||||
<span class="file-date">{{ file.modified }}</span>
|
<span class="file-date">{{ file.modified }}</span>
|
||||||
<span class="file-size">{{ file.size }}</span>
|
<span class="file-size">{{ file.size }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="delete-btn" onclick="deleteFile('{{ file.name }}', 'yml')"
|
<button class="delete-btn" onclick="deleteFile('{{ file.name }}', 'yml')" title="Видалити файл">
|
||||||
title="Видалити файл">
|
|
||||||
🗑️
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -550,10 +175,6 @@
|
|||||||
<div id="categories" class="tab-content">
|
<div id="categories" class="tab-content">
|
||||||
<div class="categories-section">
|
<div class="categories-section">
|
||||||
<div class="category-form">
|
<div class="category-form">
|
||||||
<div class="form-group">
|
|
||||||
<label for="category-id">ID категорії:</label>
|
|
||||||
<input type="text" id="category-id" placeholder="1">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="category-name">Назва категорії:</label>
|
<label for="category-name">Назва категорії:</label>
|
||||||
<input type="text" id="category-name" placeholder="Роботи-пилососи">
|
<input type="text" id="category-name" placeholder="Роботи-пилососи">
|
||||||
@@ -562,6 +183,11 @@
|
|||||||
<label for="portal-id">ID категорії Prom.ua:</label>
|
<label for="portal-id">ID категорії Prom.ua:</label>
|
||||||
<input type="text" id="portal-id" placeholder="12345">
|
<input type="text" id="portal-id" placeholder="12345">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category-url">URL категорії:</label>
|
||||||
|
<input type="text" id="category-url"
|
||||||
|
placeholder="https://www.euro.com.pl/odkurzacze-automatyczne.bhtml">
|
||||||
|
</div>
|
||||||
<button onclick="addCategory()" class="secondary-button">Додати категорію</button>
|
<button onclick="addCategory()" class="secondary-button">Додати категорію</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -570,9 +196,13 @@
|
|||||||
<h4>Збережені категорії:</h4>
|
<h4>Збережені категорії:</h4>
|
||||||
<ul id="categories-list">
|
<ul id="categories-list">
|
||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<li>
|
<li title="{{ category.url or '' }}">
|
||||||
<span>{{ category.id }} - {{ category.name }} {% if category.portal_id %} (portal_id: {{ category.portal_id }}){% endif %}</span>
|
<span>
|
||||||
<button class="delete-btn" onclick="deleteCategory('{{ category.id }}')" title="Видалити категорію">
|
{{ category.id }} - {{ category.name }}
|
||||||
|
{% if category.portal_id %} (portal_id: {{ category.portal_id }}){% endif %}
|
||||||
|
</span>
|
||||||
|
<button class="delete-btn" onclick="deleteCategory('{{ category.id }}')"
|
||||||
|
title="Видалити категорію">
|
||||||
🗑️
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -583,515 +213,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
||||||
|
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||||
<script>
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
let statusCheckInterval;
|
|
||||||
|
|
||||||
function startStatusCheck() {
|
|
||||||
// Очищаем предыдущий интервал, если он существует
|
|
||||||
if (statusCheckInterval) {
|
|
||||||
clearInterval(statusCheckInterval);
|
|
||||||
}
|
|
||||||
// Проверяем статус каждые 500мс
|
|
||||||
statusCheckInterval = setInterval(checkStatus, 500);
|
|
||||||
// Сразу делаем первую проверку
|
|
||||||
checkStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkStatus() {
|
|
||||||
fetch('/status')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
const status = document.getElementById('status');
|
|
||||||
const button = document.getElementById('parseButton');
|
|
||||||
|
|
||||||
if (data.is_running) {
|
|
||||||
let statusHtml = `
|
|
||||||
<p>
|
|
||||||
Парсинг запущено для категорії: ${data.current_category}<br>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (data.total_products > 0) {
|
|
||||||
statusHtml += `
|
|
||||||
<strong>Товар ${data.current_product} із ${data.total_products}</strong><br>
|
|
||||||
Прогрес: ${data.progress}%
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
statusHtml += `Отримання інформації про кількість товарів...`;
|
|
||||||
}
|
|
||||||
|
|
||||||
statusHtml += `</p>`;
|
|
||||||
status.innerHTML = statusHtml;
|
|
||||||
setTimeout(checkStatus, 500);
|
|
||||||
} else {
|
|
||||||
if (data.error) {
|
|
||||||
status.innerHTML = `<p class="error">Помилка: ${data.error}</p>`;
|
|
||||||
} else if (data.total_products > 0) {
|
|
||||||
status.innerHTML = `
|
|
||||||
<p>
|
|
||||||
Парсинг завершено<br>
|
|
||||||
<strong>Всього оброблено товарів: ${data.total_products}</strong>
|
|
||||||
</p>
|
|
||||||
`;
|
|
||||||
updateFilesList('parsed', 'parser', 'file-select');
|
|
||||||
}
|
|
||||||
button.disabled = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Status check error:', error);
|
|
||||||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
|
||||||
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 = '<p class="error">Будь ласка, виберіть файл для обробки</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.disabled = true;
|
|
||||||
status.innerHTML = '<p>Починаємо переклад...</p>';
|
|
||||||
|
|
||||||
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 = `<p class="error">${data.error}</p>`;
|
|
||||||
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 = `
|
|
||||||
<p>
|
|
||||||
Переклад в процесі...<br>
|
|
||||||
Оброблено: ${data.processed_items} з ${data.total_items}<br>
|
|
||||||
Прогрес: ${percent}%
|
|
||||||
</p>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
status.innerHTML = '<p>Підготовка до перекладу...</p>';
|
|
||||||
}
|
|
||||||
setTimeout(checkTranslationStatus, 1000);
|
|
||||||
} else {
|
|
||||||
if (data.error) {
|
|
||||||
status.innerHTML = `<p class="error">Помилка: ${data.error}</p>`;
|
|
||||||
} else if (data.total_items > 0) {
|
|
||||||
status.innerHTML = `
|
|
||||||
<p>
|
|
||||||
Переклад завершено<br>
|
|
||||||
Всього оброблено товарів: ${data.total_items}
|
|
||||||
</p>
|
|
||||||
`;
|
|
||||||
updateFilesList('translated', 'processor', 'yml-file-select');
|
|
||||||
}
|
|
||||||
button.disabled = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Translation status check error:', error);
|
|
||||||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
|
||||||
document.getElementById('translateButton').disabled = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 categoryId = document.getElementById('category-id').value;
|
|
||||||
const categoryName = document.getElementById('category-name').value;
|
|
||||||
const portalId = document.getElementById('portal-id').value;
|
|
||||||
|
|
||||||
if (!categoryId || !categoryName) {
|
|
||||||
alert('Будь ласка, заповніть всі поля');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('/add-category', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: categoryId,
|
|
||||||
name: categoryName,
|
|
||||||
portal_id: portalId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
// Очищаем поля ввода
|
|
||||||
document.getElementById('category-id').value = '';
|
|
||||||
document.getElementById('category-name').value = '';
|
|
||||||
document.getElementById('portal-id').value = '';
|
|
||||||
|
|
||||||
// Добавляем новую категорию в список
|
|
||||||
const categoriesList = document.getElementById('categories-list');
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.innerHTML = `
|
|
||||||
<span>${categoryId} - ${categoryName}</span>
|
|
||||||
<button class="delete-btn" onclick="deleteCategory('${categoryId}')" title="Видалити категорію">
|
|
||||||
️
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
categoriesList.appendChild(li);
|
|
||||||
|
|
||||||
// Обновляем select в генераторе YML
|
|
||||||
const ymlSelect = document.getElementById('yml-category-select');
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = categoryId;
|
|
||||||
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 = '<p class="error">Будь ласка, виберіть категорію та файл</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.disabled = true;
|
|
||||||
status.innerHTML = '<p>Генерація YML...</p>';
|
|
||||||
|
|
||||||
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 = `<p class="error">${data.error}</p>`;
|
|
||||||
} else {
|
|
||||||
status.innerHTML = '<p>YML файл успішно згенеровано</p>';
|
|
||||||
updateFilesList('yml', 'generator', null);
|
|
||||||
}
|
|
||||||
button.disabled = false;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
|
||||||
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 => `
|
|
||||||
<li>
|
|
||||||
<div class="file-info">
|
|
||||||
<a href="/download/${file.name}" class="download-link" download>
|
|
||||||
${file.name}
|
|
||||||
</a>
|
|
||||||
<span class="file-date">${file.modified}</span>
|
|
||||||
<span class="file-size">${file.size}</span>
|
|
||||||
</div>
|
|
||||||
<button class="delete-btn" onclick="deleteFile('${file.name}', '${fileType}')" title="Видалити файл">
|
|
||||||
🗑️
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// Обновляем select, если указан
|
|
||||||
if (selectId) {
|
|
||||||
const select = document.getElementById(selectId);
|
|
||||||
select.innerHTML = '<option value="">Виберіть файл...</option>' +
|
|
||||||
files.map(file => `
|
|
||||||
<option value="${file.name}">${file.name}</option>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoader(element) {
|
|
||||||
element.classList.add('loading');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideLoader(element) {
|
|
||||||
element.classList.remove('loading');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message) {
|
|
||||||
const errorDiv = document.createElement('div');
|
|
||||||
errorDiv.className = 'error-message';
|
|
||||||
errorDiv.textContent = message;
|
|
||||||
document.body.appendChild(errorDiv);
|
|
||||||
setTimeout(() => errorDiv.remove(), 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Использование:
|
|
||||||
async function someAction() {
|
|
||||||
const element = document.getElementById('someElement');
|
|
||||||
showLoader(element);
|
|
||||||
try {
|
|
||||||
const response = await fetch('/some-endpoint');
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.error) throw new Error(data.error);
|
|
||||||
// обработка успешного ответа
|
|
||||||
} catch (error) {
|
|
||||||
showError(error.message);
|
|
||||||
} finally {
|
|
||||||
hideLoader(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startParsing() {
|
|
||||||
const url = document.getElementById('url').value;
|
|
||||||
const button = document.getElementById('parseButton');
|
|
||||||
const status = document.getElementById('status');
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
status.innerHTML = '<p class="error">Будь ласка, введіть URL</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.disabled = true;
|
|
||||||
status.innerHTML = '<p>Починаємо парсинг...</p>';
|
|
||||||
|
|
||||||
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 = `<p class="error">${data.error}</p>`;
|
|
||||||
button.disabled = false;
|
|
||||||
} else {
|
|
||||||
checkParsingStatus();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
|
||||||
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 = `
|
|
||||||
<p>
|
|
||||||
Парсинг в процесі...<br>
|
|
||||||
Оброблено: ${data.processed_items} з ${data.total_items}<br>
|
|
||||||
Прогрес: ${percent}%
|
|
||||||
</p>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
status.innerHTML = '<p>Отримання інформації про товари...</p>';
|
|
||||||
}
|
|
||||||
setTimeout(checkParsingStatus, 1000);
|
|
||||||
} else {
|
|
||||||
if (data.error) {
|
|
||||||
status.innerHTML = `<p class="error">Помилка: ${data.error}</p>`;
|
|
||||||
} else {
|
|
||||||
status.innerHTML = `
|
|
||||||
<p>
|
|
||||||
Парсинг завершено<br>
|
|
||||||
Всього оброблено товарів: ${data.total_items}
|
|
||||||
</p>
|
|
||||||
`;
|
|
||||||
updateFilesList('parsed', 'parser', 'file-select');
|
|
||||||
}
|
|
||||||
button.disabled = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Status check error:', error);
|
|
||||||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
|
||||||
document.getElementById('parseButton').disabled = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshOldestCategory() {
|
|
||||||
fetch('/auto-refresh', {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
alert(`Оновлено: ${data.category}`);
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Помилка під час оновлення');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -2,79 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Вхід - Парсер mariotexno</title>
|
<title>Вхід - Парсер mariotexno</title>
|
||||||
<style>
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #1a1b26;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 100px auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.login-card {
|
|
||||||
background-color: #282a36;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.login-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.login-header h1 {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
color: #8be9fd;
|
|
||||||
}
|
|
||||||
.form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #44475a;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #1a1b26;
|
|
||||||
color: #ffffff;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #6272a4;
|
|
||||||
}
|
|
||||||
.login-button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
background-color: #7aa2f7;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
.login-button:hover {
|
|
||||||
background-color: #6b91e4;
|
|
||||||
}
|
|
||||||
.error-message {
|
|
||||||
background-color: #ff5555;
|
|
||||||
color: #ffffff;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
396
web_interface.py
396
web_interface.py
@@ -1,5 +1,20 @@
|
|||||||
from flask import Flask, render_template, request, jsonify, send_from_directory, redirect, url_for
|
from flask import (
|
||||||
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
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
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
import re
|
import re
|
||||||
from euro_scraper import create_retry_session, fetch_products, setup_selenium
|
from euro_scraper import create_retry_session, fetch_products, setup_selenium
|
||||||
@@ -12,12 +27,14 @@ from feed_generator import RobotVacuumYMLGenerator
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
BASE_URL = "https://mario.mrakells.pp.ua"
|
import time
|
||||||
|
from config import BASE_URL
|
||||||
|
|
||||||
# Добавляем в начало файла
|
# Добавляем в начало файла
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
login_manager.login_view = 'login'
|
login_manager.login_view = "login"
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin):
|
class User(UserMixin):
|
||||||
def __init__(self, id, username, password_hash):
|
def __init__(self, id, username, password_hash):
|
||||||
@@ -25,10 +42,10 @@ class User(UserMixin):
|
|||||||
self.username = username
|
self.username = username
|
||||||
self.password_hash = password_hash
|
self.password_hash = password_hash
|
||||||
|
|
||||||
|
|
||||||
# Хранилище пользователей (в реальном приложении использовать базу данных)
|
# Хранилище пользователей (в реальном приложении использовать базу данных)
|
||||||
users = {
|
users = {"mario": User(1, "mario", generate_password_hash("2htC9YlEMXAhNE"))}
|
||||||
'mario': User(1, 'mario', generate_password_hash('2htC9YlEMXAhNE'))
|
|
||||||
}
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
@@ -37,11 +54,12 @@ def load_user(user_id):
|
|||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Добавляем после создания app
|
# Добавляем после создания app
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
app.config['SECRET_KEY'] = 'your-secret-key-here' # Замените на случайный ключ
|
app.config["SECRET_KEY"] = "your-secret-key-here" # Замените на случайный ключ
|
||||||
|
|
||||||
# Глобальные настройки
|
# Глобальные настройки
|
||||||
app_settings = {"items_limit": -1} # Ограничение количества обрабатываемых товаров
|
app_settings = {"items_limit": -1} # Ограничение количества обрабатываемых товаров
|
||||||
@@ -69,6 +87,7 @@ parsing_status = {
|
|||||||
"processed_items": 0,
|
"processed_items": 0,
|
||||||
"error": None,
|
"error": None,
|
||||||
}
|
}
|
||||||
|
active_translations = set()
|
||||||
|
|
||||||
|
|
||||||
def load_categories():
|
def load_categories():
|
||||||
@@ -94,8 +113,7 @@ def extract_category(url: str) -> str:
|
|||||||
return category
|
return category
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# def start_parsing(category):
|
||||||
def start_parsing(category):
|
|
||||||
"""Запуск парсинга категории"""
|
"""Запуск парсинга категории"""
|
||||||
global parsing_status
|
global parsing_status
|
||||||
|
|
||||||
@@ -129,6 +147,48 @@ def start_parsing(category):
|
|||||||
parsing_status["is_running"] = False
|
parsing_status["is_running"] = False
|
||||||
|
|
||||||
|
|
||||||
|
def start_parsing_with_status(category_url: str):
|
||||||
|
"""Універсальний запуск парсингу з оновленням статусу"""
|
||||||
|
category = extract_category(category_url)
|
||||||
|
|
||||||
|
parsing_status.update(
|
||||||
|
{
|
||||||
|
"is_running": True,
|
||||||
|
"total_items": 0,
|
||||||
|
"processed_items": 0,
|
||||||
|
"error": None,
|
||||||
|
"current_category": category,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# ⏩ Після успішного збереження — запустити переклад
|
||||||
|
threading.Thread(
|
||||||
|
target=start_translation, args=(f"{category}_products.json",)
|
||||||
|
).start()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
parsing_status["error"] = str(e)
|
||||||
|
print(f"❌ Error during parsing {category}: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
parsing_status["is_running"] = False
|
||||||
|
|
||||||
|
|
||||||
def get_file_info(filename, directory="output"):
|
def get_file_info(filename, directory="output"):
|
||||||
"""Получение информации о файле"""
|
"""Получение информации о файле"""
|
||||||
filepath = os.path.join(directory, filename)
|
filepath = os.path.join(directory, filename)
|
||||||
@@ -139,11 +199,13 @@ def get_file_info(filename, directory="output"):
|
|||||||
"size": f"{stat.st_size / 1024:.1f} KB",
|
"size": f"{stat.st_size / 1024:.1f} KB",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_oldest_parsed_file():
|
def get_oldest_parsed_file():
|
||||||
"""Повертає найстаріший _products.json файл"""
|
"""Повертає найстаріший _products.json файл"""
|
||||||
folder = "output"
|
folder = "output"
|
||||||
files = [
|
files = [
|
||||||
f for f in os.listdir(folder)
|
f
|
||||||
|
for f in os.listdir(folder)
|
||||||
if f.endswith("_products.json") and not f.endswith("_translated_products.json")
|
if f.endswith("_products.json") and not f.endswith("_translated_products.json")
|
||||||
]
|
]
|
||||||
if not files:
|
if not files:
|
||||||
@@ -154,26 +216,28 @@ def get_oldest_parsed_file():
|
|||||||
return category
|
return category
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
username = request.form.get('username')
|
username = request.form.get("username")
|
||||||
password = request.form.get('password')
|
password = request.form.get("password")
|
||||||
|
|
||||||
user = users.get(username)
|
user = users.get(username)
|
||||||
if user and check_password_hash(user.password_hash, password):
|
if user and check_password_hash(user.password_hash, password):
|
||||||
login_user(user)
|
login_user(user)
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
return render_template('login.html', error='Неверный логин или пароль')
|
return render_template("login.html", error="Неверный логин или пароль")
|
||||||
|
|
||||||
return render_template('login.html')
|
return render_template("login.html")
|
||||||
|
|
||||||
@app.route('/logout')
|
|
||||||
|
@app.route("/logout")
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@login_required
|
@login_required
|
||||||
@@ -201,6 +265,11 @@ def index():
|
|||||||
files = [f for f in os.listdir("output/yml") if f.endswith(".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]
|
yml_files = [get_file_info(f, "output/yml") for f in files]
|
||||||
|
|
||||||
|
feed_file_info = None
|
||||||
|
feed_path = os.path.join("output/yml", "feed.yml")
|
||||||
|
if os.path.exists(feed_path):
|
||||||
|
feed_file_info = get_file_info("feed.yml", "output/yml")
|
||||||
|
|
||||||
# Загружаем категории
|
# Загружаем категории
|
||||||
categories = load_categories()
|
categories = load_categories()
|
||||||
|
|
||||||
@@ -213,25 +282,60 @@ def index():
|
|||||||
yml_files=yml_files,
|
yml_files=yml_files,
|
||||||
categories=categories,
|
categories=categories,
|
||||||
app_settings=app_settings,
|
app_settings=app_settings,
|
||||||
|
feed_file_info=feed_file_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/manual-refresh-all", methods=["POST"])
|
||||||
|
def manual_refresh_all():
|
||||||
|
threading.Thread(target=refresh_all_categories_daily).start()
|
||||||
|
return jsonify({"message": "Оновлення всіх категорій запущено вручну"})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/auto-refresh", methods=["POST"])
|
@app.route("/auto-refresh", methods=["POST"])
|
||||||
def auto_refresh():
|
def auto_refresh():
|
||||||
"""Запускає парсинг найстарішої збереженої категорії"""
|
try:
|
||||||
if parsing_status["is_running"]:
|
if parsing_status["is_running"]:
|
||||||
return jsonify({"error": "Парсинг уже запущено"})
|
return jsonify({"error": "Парсинг вже запущений"})
|
||||||
|
|
||||||
category = get_oldest_parsed_file()
|
categories = load_categories()
|
||||||
if not category:
|
|
||||||
return jsonify({"error": "Немає жодної категорії для оновлення"})
|
|
||||||
|
|
||||||
print(f"[AUTO REFRESH] Повторний парсинг для категорії: {category}")
|
valid_categories = [c for c in categories if "url" in c and c["url"]]
|
||||||
|
if not valid_categories:
|
||||||
|
return jsonify({"error": "Немає категорій з URL"})
|
||||||
|
|
||||||
thread = threading.Thread(target=start_parsing, args=(category,))
|
oldest = None
|
||||||
|
oldest_mtime = float("inf")
|
||||||
|
|
||||||
|
for cat in valid_categories:
|
||||||
|
category_url = cat["url"]
|
||||||
|
category_key = extract_category(category_url)
|
||||||
|
filename = f"{category_key}_products.json"
|
||||||
|
path = os.path.join("output", filename)
|
||||||
|
|
||||||
|
# ❗ Якщо файл не існує — одразу обираємо цю категорію
|
||||||
|
if not os.path.exists(path):
|
||||||
|
oldest = cat
|
||||||
|
break
|
||||||
|
|
||||||
|
mtime = os.path.getmtime(path)
|
||||||
|
if mtime < oldest_mtime:
|
||||||
|
oldest_mtime = mtime
|
||||||
|
oldest = cat
|
||||||
|
|
||||||
|
if not oldest:
|
||||||
|
return jsonify({"error": "Не вдалося знайти категорію для оновлення"})
|
||||||
|
|
||||||
|
# category = extract_category(oldest["url"])
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=start_parsing_with_status, args=(oldest["url"],)
|
||||||
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
return jsonify({"success": True, "category": category})
|
return jsonify({"success": True, "category": oldest["name"]})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/parse", methods=["POST"])
|
@app.route("/parse", methods=["POST"])
|
||||||
@@ -250,7 +354,7 @@ def parse():
|
|||||||
return jsonify({"error": "Парсинг уже запущен"})
|
return jsonify({"error": "Парсинг уже запущен"})
|
||||||
|
|
||||||
# Запускаем парсинг в отдельном потоке
|
# Запускаем парсинг в отдельном потоке
|
||||||
thread = threading.Thread(target=start_parsing, args=(category,))
|
thread = threading.Thread(target=start_parsing_with_status, args=(url,))
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
return jsonify({"status": "ok"})
|
return jsonify({"status": "ok"})
|
||||||
@@ -258,8 +362,21 @@ def parse():
|
|||||||
|
|
||||||
@app.route("/status")
|
@app.route("/status")
|
||||||
def get_status():
|
def get_status():
|
||||||
"""Получение статуса парсинга"""
|
# Повертаємо додатково обчислений прогрес
|
||||||
return jsonify(parsing_status)
|
progress = 0
|
||||||
|
if parsing_status["total_items"]:
|
||||||
|
progress = round(
|
||||||
|
(parsing_status["processed_items"] / parsing_status["total_items"]) * 100
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
**parsing_status,
|
||||||
|
"current_product": parsing_status["processed_items"],
|
||||||
|
"total_products": parsing_status["total_items"],
|
||||||
|
"progress": progress,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/download/<path:filename>")
|
@app.route("/download/<path:filename>")
|
||||||
@@ -326,9 +443,16 @@ def get_translation_status():
|
|||||||
|
|
||||||
|
|
||||||
def start_translation(filename: str):
|
def start_translation(filename: str):
|
||||||
"""Функция для запуска перевода в отдельнм потоке"""
|
|
||||||
global translation_status
|
global translation_status
|
||||||
|
|
||||||
|
print(f"[TRANSLATE] Починаємо переклад: {filename}")
|
||||||
|
|
||||||
|
if filename in active_translations:
|
||||||
|
print(f"[SKIP] Переклад вже йде: {filename}")
|
||||||
|
return
|
||||||
|
|
||||||
|
active_translations.add(filename)
|
||||||
|
|
||||||
translation_status["is_running"] = True
|
translation_status["is_running"] = True
|
||||||
translation_status["processed_items"] = 0
|
translation_status["processed_items"] = 0
|
||||||
translation_status["error"] = None
|
translation_status["error"] = None
|
||||||
@@ -339,6 +463,17 @@ def start_translation(filename: str):
|
|||||||
with open(os.path.join("output", filename), "r", encoding="utf-8") as f:
|
with open(os.path.join("output", filename), "r", encoding="utf-8") as f:
|
||||||
products = json.load(f)
|
products = json.load(f)
|
||||||
|
|
||||||
|
# Витягуємо category_id з назви файлу
|
||||||
|
category_id = filename.split("_")[0]
|
||||||
|
categories = load_categories()
|
||||||
|
category = next((c for c in categories if str(c["id"]) == category_id), None)
|
||||||
|
|
||||||
|
# Якщо категорія знайдена — вставляємо ID в продукти
|
||||||
|
if category and "portal_id" in category:
|
||||||
|
for product in products:
|
||||||
|
product["portal_category_id"] = category["portal_id"]
|
||||||
|
product["local_category_id"] = category["id"]
|
||||||
|
|
||||||
# Ограничиваем количество товаров только если лимит больше 0
|
# Ограничиваем количество товаров только если лимит больше 0
|
||||||
if app_settings["items_limit"] > 0:
|
if app_settings["items_limit"] > 0:
|
||||||
products = products[: app_settings["items_limit"]]
|
products = products[: app_settings["items_limit"]]
|
||||||
@@ -368,6 +503,78 @@ def start_translation(filename: str):
|
|||||||
print(f"Ошибка перевода: {e}")
|
print(f"Ошибка перевода: {e}")
|
||||||
finally:
|
finally:
|
||||||
translation_status["is_running"] = False
|
translation_status["is_running"] = False
|
||||||
|
active_translations.discard(filename)
|
||||||
|
print(f"[DONE] Переклад завершено: {filename}")
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_all_categories_daily():
|
||||||
|
"""Оновлює всі категорії один раз (але не частіше ніж раз на добу кожна)"""
|
||||||
|
if parsing_status["is_running"]:
|
||||||
|
print("[SKIP] Парсинг уже запущено. Пропускаємо автоматичне оновлення.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("[START] Щоденне оновлення категорій...")
|
||||||
|
categories = load_categories()
|
||||||
|
|
||||||
|
for category in categories:
|
||||||
|
category_name = category.get("name")
|
||||||
|
category_url = category.get("url")
|
||||||
|
|
||||||
|
if not category_url:
|
||||||
|
print(f"[SKIP] Категорія '{category_name}' не має URL")
|
||||||
|
continue
|
||||||
|
|
||||||
|
category_key = extract_category(category_url)
|
||||||
|
filename = f"{category_key}_products.json"
|
||||||
|
filepath = os.path.join("output", filename)
|
||||||
|
|
||||||
|
# ⏱ Перевіряємо, коли файл востаннє оновлювався
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
modified_time = os.path.getmtime(filepath)
|
||||||
|
age_seconds = time.time() - modified_time
|
||||||
|
if age_seconds < 86400:
|
||||||
|
print(f"[SKIP] {category_name} — оновлено менше доби тому")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"[PARSE] {category_name}")
|
||||||
|
try:
|
||||||
|
parsing_status["is_running"] = True
|
||||||
|
start_parsing_with_status(category_url)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Не вдалося оновити {category_name}: {e}")
|
||||||
|
finally:
|
||||||
|
parsing_status["is_running"] = False
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
print("[DONE] Автоматичне оновлення завершено")
|
||||||
|
|
||||||
|
|
||||||
|
def translate_all_parsed_once():
|
||||||
|
"""Одноразово запускає переклад для всіх ще не перекладених файлів"""
|
||||||
|
print("[START] Одноразовий переклад всіх категорій...")
|
||||||
|
|
||||||
|
parsed_folder = "output"
|
||||||
|
translated_folder = "output/translated"
|
||||||
|
|
||||||
|
for filename in os.listdir(parsed_folder):
|
||||||
|
if filename.endswith("_products.json") and not filename.endswith(
|
||||||
|
"_translated_products.json"
|
||||||
|
):
|
||||||
|
parsed_path = os.path.join(parsed_folder, filename)
|
||||||
|
translated_name = filename.replace(
|
||||||
|
"_products.json", "_translated_products.json"
|
||||||
|
)
|
||||||
|
translated_path = os.path.join(translated_folder, translated_name)
|
||||||
|
|
||||||
|
if os.path.exists(translated_path):
|
||||||
|
print(f"[SKIP] Вже перекладено: {filename}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"[TRANSLATE] Запускаємо переклад: {filename}")
|
||||||
|
threading.Thread(target=start_translation, args=(filename,)).start()
|
||||||
|
time.sleep(2) # трохи затримки між потоками, щоб не навантажувати
|
||||||
|
|
||||||
|
print("[DONE] Запуск перекладу завершено.")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/update-settings", methods=["POST"])
|
@app.route("/update-settings", methods=["POST"])
|
||||||
@@ -386,6 +593,75 @@ def update_settings():
|
|||||||
return jsonify({"error": str(e)})
|
return jsonify({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/manual-translate-all", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def manual_translate_all():
|
||||||
|
threading.Thread(target=translate_all_parsed_once).start()
|
||||||
|
return jsonify({"message": "Запущено переклад усіх категорій"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/generate-full-yml", methods=["POST"])
|
||||||
|
def generate_full_yml():
|
||||||
|
try:
|
||||||
|
translated_folder = "output/translated"
|
||||||
|
categories = load_categories()
|
||||||
|
all_products = []
|
||||||
|
|
||||||
|
for file in os.listdir(translated_folder):
|
||||||
|
if file.endswith("_translated_products.json"):
|
||||||
|
with open(
|
||||||
|
os.path.join(translated_folder, file), "r", encoding="utf-8"
|
||||||
|
) as f:
|
||||||
|
products = json.load(f)
|
||||||
|
|
||||||
|
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")
|
||||||
|
continue
|
||||||
|
|
||||||
|
all_products.extend(products)
|
||||||
|
|
||||||
|
if not all_products:
|
||||||
|
return jsonify({"error": "Не знайдено товарів з валідними категоріями"})
|
||||||
|
|
||||||
|
generator = RobotVacuumYMLGenerator(
|
||||||
|
base_url=BASE_URL, categories_data=categories
|
||||||
|
)
|
||||||
|
|
||||||
|
# Додаємо тільки ті категорії, що реально використовуються
|
||||||
|
added_categories = set()
|
||||||
|
for product in all_products:
|
||||||
|
cid = str(product.get("local_category_id"))
|
||||||
|
if cid and cid not in added_categories:
|
||||||
|
match = next((c for c in categories if str(c["id"]) == cid), None)
|
||||||
|
if match:
|
||||||
|
generator.add_category(cid, match["name"])
|
||||||
|
added_categories.add(cid)
|
||||||
|
|
||||||
|
output_path = "output/yml/feed.yml"
|
||||||
|
os.makedirs("output/yml", exist_ok=True)
|
||||||
|
success = generator.generate_yml(all_products, output_path)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True})
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "Помилка при генерації повного YML"})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/yml/feed.yml")
|
||||||
|
def serve_feed():
|
||||||
|
return send_from_directory("output/yml", "feed.yml", mimetype="application/xml")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/generate-yml", methods=["POST"])
|
@app.route("/generate-yml", methods=["POST"])
|
||||||
def generate_yml():
|
def generate_yml():
|
||||||
"""Обработчик генерации YML файла"""
|
"""Обработчик генерации YML файла"""
|
||||||
@@ -401,14 +677,20 @@ def generate_yml():
|
|||||||
|
|
||||||
# Загружаем категории
|
# Загружаем категории
|
||||||
categories = load_categories()
|
categories = load_categories()
|
||||||
category = next((c for c in categories if str(c["id"]) == str(category_id)), None)
|
category = next(
|
||||||
|
(c for c in categories if str(c["id"]) == str(category_id)), None
|
||||||
|
)
|
||||||
|
|
||||||
if not category:
|
if not category:
|
||||||
return jsonify({"error": "Категорія не знайдена"})
|
return jsonify({"error": "Категорія не знайдена"})
|
||||||
|
|
||||||
portal_category_id = category.get("portal_id")
|
portal_category_id = category.get("portal_id")
|
||||||
if not portal_category_id:
|
if not portal_category_id:
|
||||||
return jsonify({"error": "Категорія не має portal_id (ідентифікатор категорії Prom.ua)"})
|
return jsonify(
|
||||||
|
{
|
||||||
|
"error": "Категорія не має portal_id (ідентифікатор категорії Prom.ua)"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
os.makedirs("output/yml", exist_ok=True)
|
os.makedirs("output/yml", exist_ok=True)
|
||||||
|
|
||||||
@@ -426,7 +708,10 @@ def generate_yml():
|
|||||||
product["local_category_id"] = category["id"]
|
product["local_category_id"] = category["id"]
|
||||||
|
|
||||||
# Создаем генератор YML с указанием базового URL
|
# Создаем генератор YML с указанием базового URL
|
||||||
generator = RobotVacuumYMLGenerator(base_url=BASE_URL)
|
categories = load_categories()
|
||||||
|
generator = RobotVacuumYMLGenerator(
|
||||||
|
base_url=BASE_URL, categories_data=categories
|
||||||
|
)
|
||||||
generator.add_category(str(category["id"]), category["name"])
|
generator.add_category(str(category["id"]), category["name"])
|
||||||
|
|
||||||
# Генерируем имя выходного файла
|
# Генерируем имя выходного файла
|
||||||
@@ -453,23 +738,24 @@ def add_category():
|
|||||||
data = request.json
|
data = request.json
|
||||||
categories = load_categories()
|
categories = load_categories()
|
||||||
|
|
||||||
# Перевірка обов'язкових полів
|
# Перевірка наявності name
|
||||||
if "id" not in data or "name" not in data:
|
if "name" not in data:
|
||||||
return jsonify({"error": "Обов'язкові поля: id, name"})
|
return jsonify({"error": "Поле name є обов'язковим"})
|
||||||
|
|
||||||
# Перевірка унікальності ID
|
# Генерація нового ID (максимальний + 1 або 1)
|
||||||
if any(str(c["id"]) == str(data["id"]) for c in categories):
|
if categories:
|
||||||
return jsonify({"error": "Категорія з таким ID вже існує"})
|
new_id = max(int(c["id"]) for c in categories) + 1
|
||||||
|
else:
|
||||||
|
new_id = 1
|
||||||
|
|
||||||
# Додаємо категорію з optional portal_id
|
new_category = {"id": str(new_id), "name": data["name"]}
|
||||||
new_category = {
|
|
||||||
"id": data["id"],
|
|
||||||
"name": data["name"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if "portal_id" in data:
|
if "portal_id" in data:
|
||||||
new_category["portal_id"] = data["portal_id"]
|
new_category["portal_id"] = data["portal_id"]
|
||||||
|
|
||||||
|
if "url" in data:
|
||||||
|
new_category["url"] = data["url"]
|
||||||
|
|
||||||
categories.append(new_category)
|
categories.append(new_category)
|
||||||
save_categories(categories)
|
save_categories(categories)
|
||||||
|
|
||||||
@@ -479,14 +765,15 @@ def add_category():
|
|||||||
return jsonify({"error": str(e)})
|
return jsonify({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/delete-category", methods=["POST"])
|
@app.route("/delete-category", methods=["POST"])
|
||||||
def delete_category():
|
def delete_category():
|
||||||
"""Удаление категории"""
|
"""Видалення категорії"""
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
|
target_id = str(data["id"])
|
||||||
|
|
||||||
categories = load_categories()
|
categories = load_categories()
|
||||||
categories = [c for c in categories if c["id"] != data["id"]]
|
categories = [c for c in categories if str(c["id"]) != target_id]
|
||||||
save_categories(categories)
|
save_categories(categories)
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
|
|
||||||
@@ -602,12 +889,15 @@ def get_file_info(filename, directory):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Создаем необходимые директории
|
|
||||||
for directory in ["output", "output/translated", "output/yml"]:
|
for directory in ["output", "output/translated", "output/yml"]:
|
||||||
os.makedirs(directory, exist_ok=True)
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
# Создаем файл категорий, если его нет
|
|
||||||
if not os.path.exists(CATEGORIES_FILE):
|
if not os.path.exists(CATEGORIES_FILE):
|
||||||
save_categories([])
|
save_categories([])
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
# Запуск планувальника
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
scheduler.add_job(refresh_all_categories_daily, "interval", days=1)
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user