import json
import os
import logging
import threading
import time
from datetime import datetime
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
from werkzeug.utils import secure_filename
from dotenv import load_dotenv
import requests
import uuid
from urllib.parse import quote_plus
from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, make_response
load_dotenv()
app = Flask(__name__)
app.secret_key = 'your_unique_secret_key_meka_shop_12345_no_login'
DATA_FILE = 'data.json'
SYNC_FILES = [DATA_FILE]
REPO_ID = "Kgshop/gamemeta"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
STORE_ADDRESS = "Рынок Кербент, 5 ряд , 48 контейнер "
SHOP_NAME = "Murat боксеры оптом"
WHATSAPP_NUMBER = "996771388181"
PAYMENT_INFO = "оплату за заказ можете перевести на MBANK +996707028181"
CURRENCY_CODE = 'KGS'
CURRENCY_NAME = 'Кыргызский сом'
DOWNLOAD_RETRIES = 3
DOWNLOAD_DELAY = 5
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
translations = {
'ru': {
'admin_panel_title': 'Админ-панель ' + SHOP_NAME,
'go_to_catalog': 'Перейти в каталог',
'sync_section_title': 'Синхронизация с Датацентром',
'upload_db_button': 'Загрузить БД',
'download_db_button': 'Скачать БД',
'sync_info': 'Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.',
'categories_section_title': 'Управление категориями',
'add_category_summary': 'Добавить новую категорию',
'add_category_name_label': 'Название новой категории:',
'add_category_button': 'Добавить',
'existing_categories_title': 'Существующие категории:',
'no_categories': 'Категорий пока нет.',
'info_section_title': 'Информация',
'info_text_1': 'Управление пользователями отключено, так как сайт не требует входа.',
'info_text_2': 'Заказы создаются анонимно и должны быть подтверждены через WhatsApp.',
'products_section_title': 'Управление товарами',
'add_product_summary': 'Добавить новый товар',
'product_name_label': 'Название товара *:',
'product_price_label': 'Цена ({}) *:'.format(CURRENCY_CODE),
'product_description_label': 'Описание:',
'product_category_label': 'Категория:',
'product_photos_label': 'Фотографии (до 10 шт.):',
'product_colors_label': 'Цвета/Варианты (оставьте пустым, если нет):',
'add_color_button': 'Добавить поле для цвета/варианта',
'in_stock_label': 'В наличии',
'is_top_label': 'Топ товар (показывать наверху)',
'add_product_button': 'Добавить товар',
'product_list_title': 'Список товаров:',
'no_products': 'Товаров пока нет.',
'category_label': 'Категория:',
'price_label': 'Цена:',
'description_label': 'Описание:',
'colors_label': 'Цвета/Вар-ты:',
'no_colors': 'Нет',
'current_photos': 'Текущие фото:',
'edit_button': 'Редактировать',
'delete_button': 'Удалить',
'edit_form_title': 'Редактирование:',
'replace_photos_label': 'Заменить фотографии (выберите новые файлы, до 10 шт.):',
'save_changes_button': 'Сохранить изменения',
'default_category': 'Без категории',
'status_in_stock': 'В наличии',
'status_out_of_stock': 'Нет в наличии',
'status_top': 'Топ товар',
'confirm_upload': 'Вы уверены, что хотите принудительно загрузить локальные данные на сервер? Это перезапишет данные на сервере.',
'confirm_download': 'Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.',
'confirm_delete_category': 'Вы уверены, что хотите удалить категорию \'{}\'? Товары этой категории будут помечены как \'Без категории\'.',
'confirm_delete_product': 'Вы уверены, что хотите удалить товар \'{}\'?',
'flash_category_added': "Категория '{}' успешно добавлена.",
'flash_category_empty': "Название категории не может быть пустым.",
'flash_category_exists': "Категория '{}' уже существует.",
'flash_category_deleted': "Категория '{}' удалена. {} товаров обновлено.",
'flash_category_delete_failed': "Не удалось удалить категорию '{}'.",
'flash_product_name_price_required': "Название и цена товара обязательны.",
'flash_product_invalid_price': "Неверный формат цены.",
'flash_photo_limit': "Загружено только первые {} фото.",
'flash_photo_skipped_non_image': "Файл {} не является изображением и был пропущен.",
'flash_photo_upload_error': "Ошибка при загрузке фото {}.",
'flash_product_added': "Товар '{}' успешно добавлен.",
'flash_edit_index_missing': "Ошибка редактирования: индекс товара не передан.",
'flash_edit_index_invalid': "Ошибка редактирования: неверный индекс товара '{}'.",
'flash_edit_price_invalid': "'{}' ürünü için geçersiz fiyat formatı. Fiyat değiştirilmedi.",
'flash_photos_updated': "Фотографии товара успешно обновлены.",
'flash_old_photos_delete_error': "Не удалось удалить старые фотографии с сервера. Новые фото загружены.",
'flash_new_photos_upload_failed': "Не удалось загрузить новые фотографии (возможно, неверный формат).",
'flash_no_hf_token_photos_not_updated': "HF_TOKEN (write) не настроен. Фотографии не были обновлены.",
'flash_product_updated': "Товар '{}' успешно обновлен.",
'flash_delete_index_missing': "Ошибка удаления: индекс товара не передан.",
'flash_delete_index_invalid': "Ошибка удаления: неверный индекс товара '{}'.",
'flash_product_deleted': "Товар '{}' удален.",
'flash_photo_delete_error_deleted_local': "Товар '{}' удален локально, но фото не удалены с сервера (ошибка/токен).",
'flash_unknown_action': "Неизвестное действие: {}",
'flash_internal_error': "Произошла внутренняя ошибка при выполнении действия '{}'. Подробности в логе сервера.",
'flash_upload_success': "Данные успешно загружены на Hugging Face.",
'flash_upload_error': "Ошибка при загрузке на Hugging Face: {}",
'flash_download_success': "Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.",
'flash_download_error_hf': "Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.",
'flash_download_error_general': "Ошибка при скачивании с Hugging Face: {}",
},
'tr': {
'admin_panel_title': 'Yönetici Paneli ' + SHOP_NAME,
'go_to_catalog': 'Kataloğa Git',
'sync_section_title': 'Veri Merkezi Senkronizasyonu',
'upload_db_button': 'Veritabanını Yükle',
'download_db_button': 'Veritabanını İndir',
'sync_info': 'Yedekleme otomatik olarak her 30 dakikada bir ve her veri kaydından sonra yapılır. Anında senkronizasyon için bu düğmeleri kullanın.',
'categories_section_title': 'Kategorileri Yönet',
'add_category_summary': 'Yeni Kategori Ekle',
'add_category_name_label': 'Yeni Kategori Adı:',
'add_category_button': 'Ekle',
'existing_categories_title': 'Mevcut Kategoriler:',
'no_categories': 'Henüz kategori yok.',
'info_section_title': 'Bilgi',
'info_text_1': 'Site giriş gerektirmediği için kullanıcı yönetimi devre dışıdır.',
'info_text_2': 'Siparişler anonim olarak oluşturulur ve WhatsApp üzerinden teyit edilmelidir.',
'products_section_title': 'Ürünleri Yönet',
'add_product_summary': 'Yeni Ürün Ekle',
'product_name_label': 'Ürün Adı *:',
'product_price_label': 'Fiyat ({}) *:'.format(CURRENCY_CODE),
'product_description_label': 'Açıklama:',
'product_category_label': 'Kategori:',
'product_photos_label': 'Fotoğraflar (En fazla 10 adet):',
'product_colors_label': 'Renkler/Seçenekler (Yoksa boş bırakın):',
'add_color_button': 'Renk/Seçenek Alanı Ekle',
'in_stock_label': 'Stokta Var',
'is_top_label': 'En Popüler Ürün (üstte göster)',
'add_product_button': 'Ürün Ekle',
'product_list_title': 'Ürün Listesi:',
'no_products': 'Henüz ürün yok.',
'category_label': 'Kategori:',
'price_label': 'Fiyat:',
'description_label': 'Açıklama:',
'colors_label': 'Renkler/Seçenekler:',
'no_colors': 'Yok',
'current_photos': 'Mevcut fotoğraflar:',
'edit_button': 'Düzenle',
'delete_button': 'Sil',
'edit_form_title': 'Düzenleniyor:',
'replace_photos_label': 'Fotoğrafları Değiştir (yeni dosyaları seçin, en fazla 10 adet):',
'save_changes_button': 'Değişiklikleri Kaydet',
'default_category': 'Kategorisiz',
'status_in_stock': 'Stokta',
'status_out_of_stock': 'Stokta Yok',
'status_top': 'Popüler',
'confirm_upload': 'Yerel verileri sunucuya zorla yüklemek istediğinizden emin misiniz? Bu, sunucudaki verilerin üzerine yazacaktır.',
'confirm_download': 'Sunucudan verileri zorla indirmek istediğinizden emin misiniz? Bu, yerel dosyalarınızın üzerine yazacaktır.',
'confirm_delete_category': '\'{}\' kategorisini silmek istediğinizden emin misiniz? Bu kategorideki ürünler \'Kategorisiz\' olarak işaretlenecektir.',
'confirm_delete_product': '\'{}\' ürününü silmek istediğinizden emin misiniz?',
'flash_category_added': "'{}' kategorisi başarıyla eklendi.",
'flash_category_empty': "Kategori adı boş olamaz.",
'flash_category_exists': "'{}' kategorisi zaten mevcut.",
'flash_category_deleted': "'{}' kategorisi silindi. {} ürün güncellendi.",
'flash_category_delete_failed': "'{}' kategorisi silinemedi.",
'flash_product_name_price_required': "Ürün adı ve fiyatı zorunludur.",
'flash_product_invalid_price': "Geçersiz fiyat formatı.",
'flash_photo_limit': "Sadece ilk {} fotoğraf yüklendi.",
'flash_photo_skipped_non_image': "{} dosyası bir görsel değil ve atlandı.",
'flash_photo_upload_error': "{} fotoğrafı yüklenirken hata oluştu.",
'flash_product_added': "'{}' ürünü başarıyla eklendi.",
'flash_edit_index_missing': "Düzenleme hatası: Ürün indeksi gönderilmedi.",
'flash_edit_index_invalid': "Düzenleme hatası: Geçersiz ürün indeksi '{}'.",
'flash_edit_price_invalid': "'{}' ürünü için geçersiz fiyat formatı. Fiyat değiştirilmedi.",
'flash_photos_updated': "Ürün fotoğrafları başarıyla güncellendi.",
'flash_old_photos_delete_error': "Eski fotoğraflar sunucudan silinemedi. Yeni fotoğraflar yüklendi.",
'flash_new_photos_upload_failed': "Yeni fotoğraflar yüklenemedi (geçersiz format olabilir).",
'flash_no_hf_token_photos_not_updated': "HF_TOKEN (yazma) ayarlı değil. Fotoğraflar güncellenmedi.",
'flash_product_updated': "'{}' ürünü başarıyla güncellendi.",
'flash_delete_index_missing': "Silme hatası: Ürün indeksi gönderilmedi.",
'flash_delete_index_invalid': "Silme hatası: Geçersiz ürün indeksi '{}'.",
'flash_product_deleted': "'{}' ürünü silindi.",
'flash_photo_delete_error_deleted_local': "'{}' ürünü yerel olarak silindi ancak fotoğraflar sunucudan silinmedi (hata/token).",
'flash_unknown_action': "Bilinmeyen eylem: {}",
'flash_internal_error': "'{}' eylemi gerçekleştirilirken dahili bir hata oluştu. Sunucu günlüklerini kontrol edin.",
'flash_upload_success': "Veriler Hugging Face'e başarıyla yüklendi.",
'flash_upload_error': "Hugging Face'e yükleme hatası: {}",
'flash_download_success': "Veriler Hugging Face'ten başarıyla indirildi. Yerel dosyalar güncellendi.",
'flash_download_error_hf': "Birkaç denemeden sonra Hugging Face'ten veri indirilemedi. Günlükleri kontrol edin.",
'flash_download_error_general': "Hugging Face'ten indirme hatası: {}",
}
}
def get_admin_lang():
requested_lang = request.args.get('lang')
if requested_lang and requested_lang in translations:
lang_code = requested_lang
else:
lang_code = request.cookies.get('admin_lang', 'ru')
if lang_code not in translations:
lang_code = 'ru'
return lang_code, translations[lang_code]
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
files_to_download = [specific_file] if specific_file else SYNC_FILES
logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
all_successful = True
for file_name in files_to_download:
success = False
for attempt in range(retries + 1):
try:
logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
local_path = hf_hub_download(
repo_id=REPO_ID,
filename=file_name,
repo_type="dataset",
token=token_to_use,
local_dir=".",
local_dir_use_symlinks=False,
force_download=True,
resume_download=False
)
logging.info(f"Successfully downloaded {file_name} to {local_path}.")
success = True
break
except RepositoryNotFoundError:
logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
return False
except HfHubHTTPError as e:
if e.response.status_code == 404:
logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
if attempt == 0 and not os.path.exists(file_name):
try:
if file_name == DATA_FILE:
with open(file_name, 'w', encoding='utf-8') as f:
json.dump({'products': [], 'categories': [], 'orders': {}}, f)
logging.info(f"Created empty local file {file_name} because it was not found on HF.")
except Exception as create_e:
logging.error(f"Failed to create empty local file {file_name}: {create_e}")
success = False
break
else:
logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
except requests.exceptions.RequestException as e:
logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
except Exception as e:
logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
if attempt < retries:
time.sleep(delay)
if not success:
logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
all_successful = False
logging.info(f"Download process finished. Overall success: {all_successful}")
return all_successful
def upload_db_to_hf(specific_file=None):
if not HF_TOKEN_WRITE:
logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
return
try:
api = HfApi()
files_to_upload = [specific_file] if specific_file else SYNC_FILES
logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
for file_name in files_to_upload:
if os.path.exists(file_name):
try:
api.upload_file(
path_or_fileobj=file_name,
path_in_repo=file_name,
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
except Exception as e:
logging.error(f"Error uploading file {file_name} to Hugging Face: {e}")
else:
logging.warning(f"File {file_name} not found locally, skipping upload.")
logging.info("Finished uploading files to HF.")
except Exception as e:
logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
def periodic_backup():
backup_interval = 1800
logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
while True:
time.sleep(backup_interval)
logging.info("Starting periodic backup...")
upload_db_to_hf()
logging.info("Periodic backup finished.")
def load_data():
default_data = {'products': [], 'categories': [], 'orders': {}}
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
logging.info(f"Local data loaded successfully from {DATA_FILE}")
if not isinstance(data, dict):
logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
raise FileNotFoundError
if 'products' not in data: data['products'] = []
if 'categories' not in data: data['categories'] = []
if 'orders' not in data: data['orders'] = {}
return data
except FileNotFoundError:
logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
except json.JSONDecodeError:
logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
if download_db_from_hf(specific_file=DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
if not isinstance(data, dict):
logging.error(f"Downloaded {DATA_FILE} is not a dictionary. Using default.")
return default_data
if 'products' not in data: data['products'] = []
if 'categories' not in data: data['categories'] = []
if 'orders' not in data: data['orders'] = {}
return data
except FileNotFoundError:
logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.")
return default_data
except json.JSONDecodeError:
logging.error(f"Error decoding JSON in downloaded {DATA_FILE}. Using default.")
return default_data
except Exception as e:
logging.error(f"Unknown error loading downloaded {DATA_FILE}: {e}. Using default.", exc_info=True)
return default_data
else:
logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
if not os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(default_data, f)
logging.info(f"Created empty local file {DATA_FILE} after failed download.")
except Exception as create_e:
logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
return default_data
def save_data(data):
try:
if not isinstance(data, dict):
logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
return
if 'products' not in data: data['products'] = []
if 'categories' not in data: data['categories'] = []
if 'orders' not in data: data['orders'] = {}
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
logging.info(f"Data successfully saved to {DATA_FILE}")
upload_db_to_hf(specific_file=DATA_FILE)
except Exception as e:
logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
CATALOG_TEMPLATE = '''
VR Каталог - {{ shop_name }}
{{ shop_name }}
{% for product in products %}
{% if product.get('is_top', False) %}
Топ
{% endif %}
{% if product.get('photos') and product['photos']|length > 0 %}
{% else %}
{% endif %}