""" Mobireviewz GSMArena Importer — Bridge v2.3 """ import re import json import requests from bs4 import BeautifulSoup from http.server import HTTPServer, BaseHTTPRequestHandler # ══════════════════════════════════════════════════════════════ WP_URL = "https://mobireviewz.com" WP_USER = "YOUR_USERNAME" WP_PASSWORD = "YOUR_APP_PASSWORD" BRIDGE_PORT = 8765 # ══════════════════════════════════════════════════════════════ # ── الماركات ───────────────────────────────────────────────── BRAND_RULES = [ (r"\bRedmi\b", "Redmi"), (r"\bPOCO\b", "POCO"), (r"\bBlack Shark\b", "Black Shark"), (r"\bXiaomi\b", "Xiaomi"), (r"\biQOO\b", "iQOO"), (r"\bVivo\b", "Vivo"), (r"\bOnePlus\b", "OnePlus"), (r"\bRealme\b", "Realme"), (r"\bOPPO\b", "OPPO"), (r"\bSamsung\b", "Samsung"), (r"\bHonor\b", "Honor"), (r"\bHuawei\b", "Huawei"), (r"\biPhone\b", "Apple"), (r"\biPad\b", "Apple"), (r"\bApple\b", "Apple"), (r"\bPixel\b", "Google"), (r"\bGoogle\b", "Google"), (r"\bNokia\b", "Nokia"), (r"\bXperia\b", "Sony"), (r"\bSony\b", "Sony"), (r"\bMoto\b", "Motorola"), (r"\bMotorola\b", "Motorola"), (r"\bLenovo\b", "Lenovo"), (r"\bNubia\b", "Nubia"), (r"\bZTE\b", "ZTE"), (r"\bInfinix\b", "Infinix"), (r"\bItel\b", "Itel"), (r"\bTecno\b", "Tecno"), (r"\bROG Phone\b", "Asus"), (r"\bZenfone\b", "Asus"), (r"\bAsus\b", "Asus"), (r"\bNothing\b", "Nothing"), (r"\bLG\b", "LG"), (r"\bHTC\b", "HTC"), (r"\bMeizu\b", "Meizu"), (r"\bBlackBerry\b", "BlackBerry"), (r"\bSharp\b", "Sharp"), ] def detect_brand(name: str) -> str: for pattern, brand in BRAND_RULES: if re.search(pattern, name, re.IGNORECASE): return brand return "Unknown" # ══════════════════════════════════════════════════════════════ # 🔧 المعالجات الثابتة # ══════════════════════════════════════════════════════════════ MONTHS_AR = { "january":"يناير","february":"فبراير","march":"مارس", "april":"أبريل","may":"مايو","june":"يونيو", "july":"يوليو","august":"أغسطس","september":"سبتمبر", "october":"أكتوبر","november":"نوفمبر","december":"ديسمبر", } REGIONS_AR = { r"\bInternational\b": "الإصدار العالمي", r"\bIndia\b": "الإصدار الهندي", r"\bUSA unlocked\b": "الإصدار الأمريكي المفتوح", r"\bUSA\b": "الإصدار الأمريكي", r"\bEurope\b": "الإصدار الأوروبي", r"\bAsia\b": "الإصدار الآسيوي", r"\bChina\b": "الإصدار الصيني", r"\bLATAM\b": "إصدار أمريكا اللاتينية", r"\bAmericas\b": "إصدار الأمريكيتين", r"Africa,\s*ME,\s*APAC": "إصدار أفريقيا والشرق الأوسط وآسيا", r"\bSaudi Arabia\b": "الإصدار السعودي", r"\(market[/ ]region dependent\)": "(حسب السوق أو المنطقة)", r"\(market dependant\)": "(حسب السوق أو المنطقة)", } def translate_month(m: str) -> str: return MONTHS_AR.get(m.strip().lower(), m) def translate_regions(val: str) -> str: for pat, ar in REGIONS_AR.items(): val = re.sub(pat, ar, val, flags=re.IGNORECASE) return val # ── تاريخ الإعلان ───────────────────────────────────────────── def process_date_announced(val: str) -> str: v = val.strip() # 1993 فقط if re.match(r"^\d{4}$", v): return v # YYYY, Month DD m = re.match(r"(\d{4}),\s*(\w+)\s+(\d{1,2})$", v) if m: return f"{m.group(3)} {translate_month(m.group(2))} {m.group(1)}" # YYYY, Month m = re.match(r"(\d{4}),\s*(\w+)$", v) if m: return f"{translate_month(m.group(2))} {m.group(1)}" # YYYY, Month. Released YYYY, Month m = re.match(r"(\d{4}),\s*(\w+)\.\s*Released\s+(\d{4}),\s*(\w+)", v) if m: return f"{translate_month(m.group(2))} {m.group(1)}، صدر في {translate_month(m.group(4))} {m.group(3)}" return val # ── حالة الإصدار ────────────────────────────────────────────── def process_date_status(val: str) -> str: v = val.strip() if v.lower() == "discontinued": return "توقف إنتاجه" m = re.match(r"Coming soon\.\s*Exp\.\s*release\s+(\d{4}),\s*(\w+)\s+(\d{1,2})", v, re.I) if m: return f"قريبًا. متوقع إطلاقه في {m.group(3)} {translate_month(m.group(2))} {m.group(1)}" m = re.match(r"Coming soon\.\s*Exp\.\s*release\s+(\d{4}),\s*(\w+)", v, re.I) if m: return f"قريبًا. متوقع إطلاقه في {translate_month(m.group(2))} {m.group(1)}" if re.match(r"Coming soon\.?$", v, re.I): return "قريبًا" m = re.match(r"Available\.\s*Released\s+(\d{4}),\s*(\w+)\s+(\d{1,2})", v, re.I) if m: return f"متوفر, صدر في {m.group(3)} {translate_month(m.group(2))} {m.group(1)}" m = re.match(r"Available\.\s*Released\s+(\d{4}),\s*(\w+)", v, re.I) if m: return f"متوفر, صدر في {translate_month(m.group(2))} {m.group(1)}" return val # ── الأبعاد ─────────────────────────────────────────────────── def process_dimensions(val: str) -> str: lines = val.strip().split("\n") results = [] for line in lines: line = line.strip() if not line: continue # Unfolded: W x H x D mm m = re.match(r"Unfolded:\s*([\d.]+)\s*x\s*([\d.]+)\s*x\s*([\d.]+)\s*mm", line, re.I) if m: results.append(f"مفتوح: {m.group(1)} × {m.group(2)} × {m.group(3)} ملم") continue # Folded: W x H x D mm m = re.match(r"Folded:\s*([\d.]+)\s*x\s*([\d.]+)\s*x\s*([\d.]+)\s*mm", line, re.I) if m: results.append(f"مطوي: {m.group(1)} × {m.group(2)} × {m.group(3)} ملم") continue # W x H x D mm (W x H x D in) m = re.match( r"([\d.]+)\s*x\s*([\d.]+)\s*x\s*([\d.]+)\s*mm\s*\(([\d.]+)\s*x\s*([\d.]+)\s*x\s*([\d.]+)\s*in\)", line, re.I) if m: results.append( f"{m.group(1)} × {m.group(2)} × {m.group(3)} ملم" f" - {m.group(4)} × {m.group(5)} × {m.group(6)} إنش" ) continue # W x H x D mm or D2 mm m = re.match(r"([\d.]+)\s*x\s*([\d.]+)\s*x\s*([\d.]+)\s*mm\s+or\s+([\d.]+)\s*mm", line, re.I) if m: results.append(f"{m.group(1)} × {m.group(2)} × {m.group(3)} ملم أو {m.group(4)} ملم") continue # W x H x D mm m = re.match(r"([\d.]+)\s*x\s*([\d.]+)\s*x\s*([\d.]+)\s*mm", line, re.I) if m: results.append(f"{m.group(1)} × {m.group(2)} × {m.group(3)} ملم") continue results.append(line) return "\n".join(results) # ── الوزن ───────────────────────────────────────────────────── def process_weight(val: str) -> str: v = val.strip() # 34 g (44mm), 30 g (40mm) (1.06 oz) m = re.match(r"([\d.]+)\s*g\s*\(([\d.]+mm)\)\s*,\s*([\d.]+)\s*g\s*\(([\d.]+mm)\)", v, re.I) if m: return f"{m.group(1)} جرام ({m.group(2)}), {m.group(3)} جرام ({m.group(4)})" # 195 g or 198 g m = re.match(r"([\d.]+)\s*g\s+or\s+([\d.]+)\s*g", v, re.I) if m: return f"{m.group(1)} جرام أو {m.group(2)} جرام" # 179 g (6.31 oz) m = re.search(r"([\d.]+)\s*g\b", v, re.I) if m: return f"{m.group(1)} جرام" return val # ── البناء — المحلل الذكي ───────────────────────────────────── """ المنطق: نحلّل النص إلى مكونات (أمام، إطار، ظهر، ملحقات) ثم نُعيد بناءه بالعربية بترتيب ثابت. """ # مصطلحات المواد → عربي MATERIAL_AR = { # زجاج الأمام r"glass front": "زجاج من الأمام", r"plastic front": "بلاستيك من الأمام", r"sapphire crystal front": "ياقوت كريستالي من الأمام", r"sapphire glass": "زجاج ياقوتي", # الإطارات r"aluminum alloy frame": "الإطار من سبيكة الألومنيوم", r"aluminum frame": "الإطار من الألومنيوم", r"titanium alloy frame": "الإطار من سبيكة التيتانيوم", r"titanium frame \(grade 5\)": "الإطار من التيتانيوم (الدرجة الخامسة)", r"titanium frame \(grade 4\)": "الإطار من التيتانيوم (الدرجة الرابعة)", r"titanium frame \(grade 3\)": "الإطار من التيتانيوم (الدرجة الثالثة)", r"titanium frame \(grade 2\)": "الإطار من التيتانيوم (الدرجة الثانية)", r"titanium frame \(grade 1\)": "الإطار من التيتانيوم (الدرجة الأولى)", r"titanium frame": "الإطار من التيتانيوم", r"stainless steel frame 316L": "الإطار من الفولاذ المقاوم للصدأ 316L", r"stainless steel 316L frame": "الإطار من الفولاذ المقاوم للصدأ 316L", r"stainless steel frame": "الإطار من الفولاذ المقاوم للصدأ", r"plastic frame": "الإطار من البلاستيك", r"ceramic frame": "الإطار من السيراميك", r"zinc alloy frame": "الإطار من سبيكة الزنك", r"aluminum/plastic frame": "الإطار من الألومنيوم أو البلاستيك", r"glass-fiber reinforced plastic frame": "الإطار من البلاستيك المقوى بالألياف الزجاجية", r"polymer fiber frame": "الإطار من ألياف البوليمر", r"titanium alloy bezel": "الإطار الخارجي من سبيكة التيتانيوم", r"aluminum alloy mid-frame": "الإطار الداخلي من سبيكة الألومنيوم", r"aluminum alloy bezel": "حافة من سبيكة الألومنيوم", r"aluminum frame \(6000 series\)": "الإطار من الألومنيوم (سلسلة 6000)", # الظهر r"glass-fiber reinforced plastic back": "الظهر من البلاستيك المقوى بالألياف الزجاجية", r"ceramic-glass fiber-reinforced polymer back": "الظهر من البوليمر المقوى بالألياف الزجاجية والسيراميك", r"silicone polymer back \(eco leather\)": "الظهر من بوليمر السيليكون (جلد صناعي صديق للبيئة)", r"silicone polymer \(eco leather\) back": "الظهر من بوليمر السيليكون (جلد صناعي صديق للبيئة)", r"silicone polymer \(vegan leather\) back": "الظهر من بوليمر السيليكون (جلد نباتي)", r"silicone polymer back": "الظهر من بوليمر السيليكون", r"aluminum alloy back": "الظهر من سبيكة الألومنيوم", r"aluminum/plastic back": "الظهر من الألومنيوم أو البلاستيك", r"aluminum/glass back": "الظهر من الألومنيوم أو الزجاج", r"aluminum back": "الظهر من الألومنيوم", r"plastic back": "الظهر من البلاستيك", r"ceramic back": "الظهر من السيراميك", r"glass back": "الظهر من الزجاج", r"eco leather back": "الظهر من الجلد الصناعي", r"leather back": "الظهر من الجلد", r"matte glass back": "الظهر من الزجاج المطفأ", r"nylon fiber back": "الظهر من ألياف النايلون", r"polymer fiber back": "الظهر من ألياف البوليمر", r"aramid fibre back": "الظهر من ألياف الأراميد", # ملحقات r"hinge \(stainless steel\)": "المفصل من الفولاذ المقاوم للصدأ", r"hinge \(titanium\)": "المفصل من التيتانيوم", r"steel hinge": "المفصل من الفولاذ", r"titanium hinge housing": "هيكل المفصل من التيتانيوم", r"titanium alloy hinge \(grade 5\)": "مفصل من سبيكة التيتانيوم (الدرجة الخامسة)", r"aluminum alloy frame": "الإطار من سبيكة الألومنيوم", # أحوال الطي r"\bfolded\b": "مطوي", r"\bunfolded\b": "مفتوح", r"\bclosed\b": "مغلق", # متفرقات r"Sapphire crystal": "ياقوت كريستالي", r"Sapphire Glass": "زجاج ياقوتي", r"\bor\b": "أو", r"\bwith\b": "مع", r"grade 5": "الدرجة الخامسة", r"grade 4": "الدرجة الرابعة", r"grade 3": "الدرجة الثالثة", r"grade 2": "الدرجة الثانية", r"grade 1": "الدرجة الأولى", r"6000 series": "سلسلة 6000", r"316L": "316L", r"aluminum alloy": "سبيكة الألومنيوم", r"titanium alloy": "سبيكة التيتانيوم", r"\baluminum\b": "الألومنيوم", r"\btitanium\b": "التيتانيوم", r"\bplastic\b": "البلاستيك", r"\bceramic\b": "السيراميك", r"\bglass\b": "الزجاج", r"stainless steel": "الفولاذ المقاوم للصدأ", r"polymer fiber": "ألياف البوليمر", r"nylon fiber": "ألياف النايلون", r"aramid fibre": "ألياف الأراميد", r"eco leather": "جلد صناعي صديق للبيئة", r"vegan leather": "جلد نباتي", r"zirconium": "الزركونيوم", } def process_build(val: str) -> str: """ محلل ذكي لحقل البناء: - يطبق الاستبدالات من الأطول للأقصر لتجنب التعارض - يحافظ على النصوص بين الأقواس (أسماء الزجاج) كما هي - يترجم الكلمات المفتاحية المعروفة فقط """ result = val.strip() # أولاً: احفظ النصوص بين الأقواس مؤقتاً placeholders = {} idx = 0 def save_parens(m): nonlocal idx key = f"__PAREN{idx}__" # ترجم بعض المحتوى داخل الأقواس content = m.group(1) content = re.sub(r"\bfolded\b", "مطوي", content, flags=re.I) content = re.sub(r"\bunfolded\b", "مفتوح", content, flags=re.I) content = re.sub(r"\bclosed\b", "مغلق", content, flags=re.I) content = re.sub(r"\bgrade 5\b", "الدرجة الخامسة", content, flags=re.I) content = re.sub(r"\bgrade 4\b", "الدرجة الرابعة", content, flags=re.I) content = re.sub(r"\bgrade 3\b", "الدرجة الثالثة", content, flags=re.I) content = re.sub(r"\bgrade 2\b", "الدرجة الثانية", content, flags=re.I) content = re.sub(r"\bgrade 1\b", "الدرجة الأولى", content, flags=re.I) content = re.sub(r"\bSapphire Glass\b", "زجاج ياقوتي", content, flags=re.I) content = re.sub(r"\bSapphire crystal\b", "ياقوت كريستالي", content, flags=re.I) content = re.sub(r"6000 series", "سلسلة 6000", content, flags=re.I) placeholders[key] = f"({content})" idx += 1 return key result = re.sub(r"\(([^)]+)\)", save_parens, result) # طبّق الاستبدالات من الأطول للأقصر for pattern, ar in sorted(MATERIAL_AR.items(), key=lambda x: len(x[0]), reverse=True): result = re.sub(pattern, ar, result, flags=re.IGNORECASE) # أعد الأقواس for key, paren in placeholders.items(): result = result.replace(key, paren) # نظّف المسافات الزائدة حول الفواصل result = re.sub(r"\s*,\s*", ", ", result) result = re.sub(r"\s+", " ", result).strip() return result def process_yes_no(val: str) -> str: v = val.strip().lower() if v == "yes": return "نعم" if v == "no": return "لا" return val # ══════════════════════════════════════════════════════════════ # 🎛️ تطبيق القواعد المخصصة من WordPress # قواعد Regex يكتبها المستخدم في لوحة التحكم # الصيغة: list of {"pattern": "...", "replacement": "...", "field": "all|field_id"} # ══════════════════════════════════════════════════════════════ def apply_custom_rules(val: str, field_id: str, custom_rules: list) -> str: for rule in custom_rules: target = rule.get("field", "all") if target != "all" and target != field_id: continue pattern = rule.get("pattern", "") replacement = rule.get("replacement", "") flags_str = rule.get("flags", "i") if not pattern: continue flags = re.IGNORECASE if "i" in flags_str else 0 try: val = re.sub(pattern, replacement, val, flags=flags) except re.error: pass # تجاهل القواعد الخاطئة return val # ══════════════════════════════════════════════════════════════ # 🌐 سحب المواصفات — مع دعم الحقول متعددة الأسطر # ══════════════════════════════════════════════════════════════ def fetch_specs(url: str) -> tuple: r = requests.get(url, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.gsmarena.com/", }, timeout=20) r.raise_for_status() soup = BeautifulSoup(r.text, "html.parser") name_tag = soup.find("h1", class_="specs-phone-name-title") phone_name = name_tag.get_text(strip=True) if name_tag else "Unknown" img_tag = soup.select_one("div.specs-photo-main img") image_url = img_tag["src"] if img_tag and img_tag.get("src") else None specs = {} last_key = None last_sec = None for table in soup.select("table"): th = table.find("th") section = th.get_text(strip=True) if th else "General" for row in table.select("tr"): ttl = row.select_one("td.ttl") nfo = row.select_one("td.nfo") if not nfo: continue lines = list(nfo.stripped_strings) val = "\n".join(lines).strip() if not val: continue if ttl and ttl.get_text(strip=True): key = ttl.get_text(strip=True) last_key = key last_sec = section else: if last_key and last_sec == section: full_key = f"{last_sec}::{last_key}" prev = specs.get(full_key, "") combined = (prev + "\n" + val).strip() if prev else val specs[full_key] = combined specs[last_key] = combined specs[last_key.lower()] = combined continue specs[f"{section}::{key}"] = val specs[key] = val specs[key.lower()] = val return phone_name, specs, image_url # ── البحث المرن ─────────────────────────────────────────────── def lookup(specs: dict, gsm_key: str) -> str: gsm_key = gsm_key.strip() if gsm_key in specs: return specs[gsm_key] short = gsm_key.split("::")[-1].strip() if short in specs: return specs[short] if short.lower() in specs: return specs[short.lower()] short_l = short.lower() for k, v in specs.items(): if "::" in k and short_l == k.split("::")[-1].strip().lower(): return v return "" # ══════════════════════════════════════════════════════════════ # 🗂️ بناء ACF # ══════════════════════════════════════════════════════════════ PROCESSORS = { "dimensions": process_dimensions, "weight": process_weight, "yes_no": process_yes_no, "build": process_build, "date_announced": process_date_announced, "date_status": process_date_status, "regions": translate_regions, } def build_acf(specs: dict, mappings: str, custom_rules: list) -> dict: acf_data = {} written = set() for line in mappings.strip().split("\n"): line = line.strip() if not line or "|" not in line: continue parts = line.split("|") gsm_key = parts[0].strip() acf_path = parts[1].strip() processor = parts[2].strip() if len(parts) > 2 else None if ":" not in acf_path: continue group, field = acf_path.split(":", 1) group = group.strip() field = field.strip() field_id = f"{group}:{field}" if field_id in written: continue raw = lookup(specs, gsm_key) if not raw: continue # طبّق المعالج الثابت if processor and processor in PROCESSORS: final = PROCESSORS[processor](raw) else: final = raw # ترجمة المناطق تلقائياً لحقول الشبكة if not processor and any(x in gsm_key for x in ["bands", "2G", "3G", "4G", "5G"]): final = translate_regions(final) # طبّق القواعد المخصصة من WordPress final = apply_custom_rules(final, field_id, custom_rules) if group not in acf_data: acf_data[group] = {} acf_data[group][field] = final written.add(field_id) preview = str(final)[:65].replace("\n", " | ") print(f" ✓ {field_id} = {preview}") return acf_data # ── JWT ─────────────────────────────────────────────────────── def get_token() -> str | None: r = requests.post( f"{WP_URL}/wp-json/jwt-auth/v1/token", json={"username": WP_USER, "password": WP_PASSWORD}, timeout=10, ) if r.status_code == 200: return r.json().get("token") print(f"❌ JWT: {r.text[:200]}") return None def get_or_create_brand(brand: str, token: str) -> int | None: h = {"Authorization": f"Bearer {token}"} r = requests.get(f"{WP_URL}/wp-json/wp/v2/mobile-phone-brands", params={"search": brand, "per_page": 10}, headers=h, timeout=10) if r.ok: for t in r.json(): if t.get("name", "").lower() == brand.lower(): print(f"🏷️ {brand} (ID:{t['id']})") return t["id"] r = requests.post(f"{WP_URL}/wp-json/wp/v2/mobile-phone-brands", headers=h, json={"name": brand}, timeout=10) if r.ok and r.status_code in [200, 201]: tid = r.json().get("id") print(f"🏷️ ماركة جديدة: {brand} (ID:{tid})") return tid return None def upload_image(url: str, name: str, token: str) -> int | None: try: img = requests.get(url, timeout=15).content fn = re.sub(r"[^\w-]", "-", name.lower()) + ".jpg" r = requests.post( f"{WP_URL}/wp-json/wp/v2/media", headers={"Authorization": f"Bearer {token}", "Content-Disposition": f'attachment; filename="{fn}"', "Content-Type": "image/jpeg"}, data=img, timeout=30) if r.status_code in [200, 201]: mid = r.json().get("id") print(f"🖼️ Media ID:{mid}") return mid except Exception as e: print(f"⚠️ صورة: {e}") return None # ── الاستيراد ───────────────────────────────────────────────── def run_import(url: str, mappings: str, custom_rules: list) -> dict: token = get_token() if not token: return {"success": False, "message": "فشل المصادقة"} try: phone_name, specs, image_url = fetch_specs(url) except Exception as e: return {"success": False, "message": f"فشل سحب الصفحة: {e}"} brand = detect_brand(phone_name) print(f"\n📱 {phone_name} | 🏷️ {brand}") print(f"📊 {len(specs)//3} حقل") acf_data = build_acf(specs, mappings, custom_rules) media_id = upload_image(image_url, phone_name, token) if image_url else None brand_term_id = get_or_create_brand(brand, token) if brand != "Unknown" else None payload = { "title": phone_name, "status": "draft", "content": "", "acf": acf_data, "meta": {"rank_math_focus_keyword": phone_name}, } if media_id: payload["featured_media"] = media_id if brand_term_id: payload["mobile-phone-brands"] = [brand_term_id] r = requests.post(f"{WP_URL}/wp-json/wp/v2/mobiles", headers={"Authorization": f"Bearer {token}"}, json=payload, timeout=30) if r.status_code in [200, 201]: post_id = r.json().get("id") print(f"✅ Post ID: {post_id}") return {"success": True, "message": f"تم استيراد {phone_name}", "post_id": post_id, "edit_url": f"{WP_URL}/wp-admin/post.php?post={post_id}&action=edit"} print(f"❌ {r.status_code}: {r.text[:300]}") return {"success": False, "message": f"فشل النشر ({r.status_code})"} def run_debug(url: str) -> dict: try: phone_name, specs, _ = fetch_specs(url) except Exception as e: return {"success": False, "message": str(e)} sections = {} for k, v in specs.items(): if "::" not in k: continue sec, field = k.split("::", 1) sections.setdefault(sec, {})[field] = v return {"success": True, "phone_name": phone_name, "sections": sections} # ══════════════════════════════════════════════════════════════ # 🌉 Bridge # ══════════════════════════════════════════════════════════════ class Handler(BaseHTTPRequestHandler): def log_message(self, *a): pass def _cors(self): self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") def _json(self, data, code=200): body = json.dumps(data, ensure_ascii=False).encode("utf-8") self.send_response(code) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", len(body)) self._cors() self.end_headers() self.wfile.write(body) def do_OPTIONS(self): self.send_response(200); self._cors(); self.end_headers() def do_GET(self): self._json({"status": "ok"}) if self.path == "/ping" \ else self._json({"error": "not found"}, 404) def do_POST(self): try: length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length)) except Exception as e: self._json({"success": False, "message": str(e)}); return if self.path == "/import": url = body.get("url", "") mappings = body.get("mappings", "") custom_rules = body.get("custom_rules", []) if not url or "gsmarena.com" not in url: self._json({"success": False, "message": "رابط غير صحيح"}); return print(f"\n{'═'*52}\n📥 {url}") self._json(run_import(url, mappings, custom_rules)) elif self.path == "/debug": print(f"\n🔍 {body.get('url','')}") self._json(run_debug(body.get("url", ""))) else: self._json({"error": "not found"}, 404) if __name__ == "__main__": server = HTTPServer(("localhost", BRIDGE_PORT), Handler) print("═" * 52) print(f" 🚀 Mobireviewz Bridge v2.3") print(f" ✅ جاهز على المنفذ {BRIDGE_PORT}") print(f" ⏹ للإيقاف: أغلق هذه النافذة") print("═" * 52) try: server.serve_forever() except KeyboardInterrupt: print("\n⏹ تم الإيقاف")
Warning: Cannot modify header information - headers already sent by (output started at /home/u957087459/domains/mobireviewz.com/public_html/wp-content/plugins/mobireviewz-suite/importer/class-importer-admin.php:1) in /home/u957087459/domains/mobireviewz.com/public_html/wp-includes/pluggable.php on line 1531

Warning: Cannot modify header information - headers already sent by (output started at /home/u957087459/domains/mobireviewz.com/public_html/wp-content/plugins/mobireviewz-suite/importer/class-importer-admin.php:1) in /home/u957087459/domains/mobireviewz.com/public_html/wp-includes/pluggable.php on line 1534