Compare commits

..

4 Commits

Author SHA1 Message Date
qananasikq
47221dc1c8 add limit cap and remove unused import 2026-04-16 20:44:27 +03:00
qananasikq
f47081d273 escape ilike params in cars route 2026-04-16 20:44:18 +03:00
qananasikq
930057b6b9 schedule next sync one hour after finish 2026-04-16 20:44:11 +03:00
qananasikq
90f5dff25c move translations to new file 2026-04-16 20:44:05 +03:00
7 changed files with 760 additions and 403 deletions

View File

@@ -28,9 +28,11 @@ def list_cars(
query = select(Car)
if brand:
query = query.where(Car.brand.ilike(f"%{brand}%"))
escaped_brand = brand.replace("%", r"\%").replace("_", r"\_")
query = query.where(Car.brand.ilike(f"%{escaped_brand}%", escape="\\"))
if model:
query = query.where(Car.model.ilike(f"%{model}%"))
escaped_model = model.replace("%", r"\%").replace("_", r"\_")
query = query.where(Car.model.ilike(f"%{escaped_model}%", escape="\\"))
if year_min is not None:
query = query.where(Car.year >= year_min)
if year_max is not None:
@@ -62,7 +64,9 @@ def get_car(
):
# Детальная информация об автомобиле с изображениями
with persistence.session_scope() as session:
car = session.get(Car, car_id)
car = session.execute(
select(Car).options(selectinload(Car.images)).where(Car.id == car_id)
).scalars().first()
if car is None:
raise HTTPException(status_code=404, detail="Car not found")
return CarRead.model_validate(car).model_dump(mode="json")
@@ -76,7 +80,7 @@ def get_car_by_origin(
# Поиск автомобиля по origin_id
with persistence.session_scope() as session:
car = session.execute(
select(Car).where(Car.origin_id == origin_id)
select(Car).options(selectinload(Car.images)).where(Car.origin_id == origin_id)
).scalars().first()
if car is None:
raise HTTPException(status_code=404, detail="Car not found")

View File

@@ -1,7 +1,7 @@
# Роуты запуска задач синхронизации Encar и просмотра истории sync-runs.
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from sqlalchemy import select, func
from ..deps import get_persistence
@@ -25,7 +25,7 @@ class SyncListingRequest(BaseModel):
year_to: int | None = None
price_max: int | None = None
lane: str = "encar"
limit: int | None = None
limit: int | None = Field(default=None, le=500_000)
@router.post("/tasks/sync-vehicle")

View File

@@ -1,7 +1,7 @@
import json
import re
from pathlib import Path
from typing import Any, Callable, Iterable
from typing import Any, Iterable
def save_to_json(data: Any, filename: str | Path) -> None:

View File

@@ -15,6 +15,19 @@ import urllib3
from .core.config import Settings
from .storage.db import PersistenceService
from .storage.schemas import CarRecord, ImageRecord
from .translations import (
BODY_TYPE_MAP,
BRAND_ENGLISH_ALIASES,
BRAND_TRANSLATIONS,
CITY_TO_COUNTRY,
COLOR_TRANSLATIONS,
FUEL_TYPE_MAP,
GEARBOX_MAP_KO,
MODEL_BODY_TYPE_MAP,
MODEL_TRANSLATIONS,
SELL_TYPE_MAP,
expand_allowed_brands,
)
logger = logging.getLogger("encar_scraper")
@@ -38,387 +51,6 @@ CAR_TYPE_DOMESTIC = "Y" # только отечественные
CAR_TYPE_IMPORT = "N" # только импорт
CAR_TYPE_ELECTRIC = "G" # электро/гибрид
# Корейские → латинские для fuel
FUEL_TYPE_MAP = {
"가솔린": "petrol",
"gasoline": "petrol",
"디젤": "diesel",
"diesel": "diesel",
"전기": "electric",
"electric": "electric",
"하이브리드": "hybrid",
"hybrid": "hybrid",
"lpg": "lpg",
"LPG": "lpg",
"LPG(일반인 구입)": "lpg",
"LPG(장애인 구입)": "lpg",
"가솔린+전기": "hybrid",
"디젤+전기": "hybrid",
"CNG": "cng",
}
GEARBOX_MAP_KO = {
"오토": "AT",
"자동": "AT",
"수동": "MT",
"cvt": "CVT",
"CVT": "CVT",
}
SELL_TYPE_MAP = {
"일반": "STOCK",
"경매": "AUCTION",
"입찰": "TENDER",
}
CITY_TO_COUNTRY = {
"서울": "KR", "경기": "KR", "인천": "KR", "부산": "KR", "대구": "KR",
"광주": "KR", "대전": "KR", "울산": "KR", "세종": "KR", "강원": "KR",
"충북": "KR", "충남": "KR", "전북": "KR", "전남": "KR", "경북": "KR",
"경남": "KR", "제주": "KR",
}
# Переводы брендов с корейского/английского на русский
BRAND_TRANSLATIONS = {
"현대": "Хёндэ",
"기아": "Киа",
"쉐보레(GM대우)": "Шевроле",
"벤츠": "Мерседес-Бенц",
"BMW": "BMW",
"아우디": "Ауди",
"폭스바겐": "Фольксваген",
"토요타": "Тойота",
"도요타": "Тойота",
"닛산": "Ниссан",
"혼다": "Хонда",
"포르쉐": "Порше",
"랜드로버": "Ленд Ровер",
"재규어": "Ягуар",
"볼보": "Вольво",
"푸조": "Пежо",
"시트로엥": "Ситроен",
"르노": "Рено",
"미니": "Мини",
"링컨": "Линкольн",
"캐딜락": "Кадиллак",
"크라이슬러": "Крайслер",
"지프": "Джип",
"포드": "Форд",
"테슬라": "Тесла",
"폴크스바겐": "Фольксваген",
"스코다": "Шкода",
"세아트": "SEAT",
"오펠": "Опель",
"피아트": "Фиат",
"알파로메오": "Альфа Ромео",
"마세라티": "Мазерати",
"람보르기니": "Ламборгини",
"페라리": "Феррари",
"벤틀리": "Бентли",
"롤스로이스": "Роллс-Ройс",
"애스턴마틴": "Астон Мартин",
"맥라렌": "Макларен",
"부가티": "Бугатти",
"코닉세그": "Кёнигсегг",
"파가니": "Пагани",
"헤네시": "Hennessey",
"쉘비": "Shelby",
"다임러": "Daimler",
"마이바흐": "Майбах",
"스마트": "Смарт",
"DS": "DS",
"렉서스": "Лексус",
"인피니티": "Инфинити",
"아큐라": "Акура",
"스바루": "Субару",
"미쓰비시": "Мицубиси",
"마쓰다": "Мазда",
"이스즈": "Исузу",
"다이하츠": "Daihatsu",
"스즈키": "Сузуки",
"KG모빌리티(쌍용)": "KG Mobility",
"르노코리아(삼성)": "Рено Корея",
"한국GM": "GM Korea",
"쌍용": "СсангЙонг",
"대우": "Дэу",
"제네시스": "Генезис",
"르노삼성": "Рено Самсунг",
"닷지": "Dodge",
"험머": "Hummer",
"폴스타": "Polestar",
"사브": "Saab",
"로터스": "Lotus",
"리비안": "Rivian",
"루시드": "Lucid",
"리막": "Rimac",
"알파 로메오": "Альфа Ромео",
"마쯔다": "Мазда",
"어큐라": "Акура",
"다이하쯔": "Daihatsu",
}
# Маппинг английских названий брендов → все варианты (корейский + русский перевод).
# Используется для расширения фильтра allowed_brands из runtime_config.
BRAND_ENGLISH_ALIASES: dict[str, list[str]] = {
"hyundai": ["현대", "Хёндэ"],
"kia": ["기아", "Киа"],
"genesis": ["제네시스", "Генезис"],
"chevrolet": ["쉐보레(GM대우)", "쉐보레", "Шевроле"],
"mercedes": ["벤츠", "Мерседес-Бенц"],
"mercedes-benz": ["벤츠", "Мерседес-Бенц"],
"bmw": ["BMW"],
"audi": ["아우디", "Ауди"],
"volkswagen": ["폭스바겐", "폴크스바겐", "Фольксваген"],
"toyota": ["토요타", "도요타", "Тойота"],
"nissan": ["닛산", "Ниссан"],
"honda": ["혼다", "Хонда"],
"porsche": ["포르쉐", "Порше"],
"land rover": ["랜드로버", "Ленд Ровер"],
"jaguar": ["재규어", "Ягуар"],
"volvo": ["볼보", "Вольво"],
"peugeot": ["푸조", "Пежо"],
"citroen": ["시트로엥", "Ситроен", "시트로엥/DS"],
"renault": ["르노", "Рено"],
"mini": ["미니", "Мини"],
"lincoln": ["링컨", "Линкольн"],
"cadillac": ["캐딜락", "Кадиллак"],
"chrysler": ["크라이슬러", "Крайслер"],
"jeep": ["지프", "Джип"],
"ford": ["포드", "Форд"],
"tesla": ["테슬라", "Тесла"],
"lexus": ["렉서스", "Лексус"],
"infiniti": ["인피니티", "Инфинити"],
"acura": ["아큐라", "Акура"],
"subaru": ["스바루", "Субару"],
"mitsubishi": ["미쓰비시", "Мицубиси"],
"mazda": ["마쓰다", "Мазда"],
"suzuki": ["스즈키", "Сузуки"],
"ferrari": ["페라리", "Феррари"],
"lamborghini": ["람보르기니", "Ламборгини"],
"maserati": ["마세라티", "Мазерати"],
"bentley": ["벤틀리", "Бентли"],
"rolls-royce": ["롤스로이스", "Роллс-Ройс"],
"aston martin": ["애스턴마틴", "Астон Мартин"],
"mclaren": ["맥라렌", "Макларен"],
"ssangyong": ["쌍용", "СсангЙонг", "KG모빌리티(쌍용)", "KG Mobility"],
"daewoo": ["대우", "Дэу"],
"dodge": ["닷지"],
"hummer": ["험머"],
"gmc": ["GMC"],
"polestar": ["폴스타"],
"ds": ["DS", "시트로엥/DS"],
"renault korea": ["르노코리아(삼성)", "Рено Корея", "르노삼성", "Рено Самсунг"],
"saab": ["사브"],
"lotus": ["로터스"],
"alfa romeo": ["알파로메오", "Альфа Ромео", "알파 로메오"],
"fiat": ["피아트", "Фиат"],
"smart": ["스마트", "Смарт"],
"skoda": ["스코다", "Шкода"],
"daihatsu": ["다이하츠", "다이하쯔", "Daihatsu"],
"koenigsegg": ["코닉세그", "Кёнигсегг"],
"pagani": ["파가니", "Пагани"],
"bugatti": ["부가티", "Бугатти"],
"maybach": ["마이바흐", "Майбах"],
"rivian": ["리비안"],
"lucid": ["루시드"],
"rimac": ["리막"],
}
def expand_allowed_brands(brands: set[str] | None) -> set[str] | None:
"""Расширяет набор английских брендов всеми известными alias'ами (корейский + русский)."""
if not brands:
return brands
expanded = set(brands)
for brand in brands:
aliases = BRAND_ENGLISH_ALIASES.get(brand.lower(), [])
for alias in aliases:
expanded.add(alias.lower())
return expanded
MODEL_TRANSLATIONS = {
"가솔린": "бензин",
"디젤": "дизель",
"하이브리드": "гибрид",
"전기": "электро",
"터보": "турбо",
"쿠페": "купе",
"세단": "седан",
"왜건": "универсал",
"해치백": "хэтчбек",
"컨버터블": "кабриолет",
"오토": "автомат",
"수동": "механика",
"프레스티지": "престиж",
"시그니처": "сигнатур",
"럭셔리": "люкс",
"스포츠": "спорт",
"롱레인지": "дальний запас хода",
"스탠다드": "стандарт",
"플러스": "плюс",
"리미티드": "лимитед",
"스페셜": "спешл",
"모던": "модерн",
"익스클루시브": "эксклюзив",
"캘리그래피": "каллиграфи",
"노블레스": "ноблесс",
"트림": "трим",
"후륜": "задний привод",
"전륜": "передний привод",
"사륜": "полный привод",
}
# Переводы цветов с корейского на английский
COLOR_TRANSLATIONS = {
"흰색": "white",
"검정색": "black",
"회색": "gray",
"은색": "silver",
"진회색": "dark gray",
"밝은 회색": "light gray",
"청색": "blue",
"파란색": "blue",
"진청색": "dark blue",
"밝은 청색": "light blue",
"녹색": "green",
"초록색": "green",
"진녹색": "dark green",
"밝은 녹색": "light green",
"적색": "red",
"빨간색": "red",
"진적색": "dark red",
"밝은 적색": "light red",
"주황색": "orange",
"노란색": "yellow",
"갈색": "brown",
"베이지색": "beige",
"아이보리": "ivory",
"크림색": "cream",
"보라색": "purple",
"분홍색": "pink",
"금색": "gold",
"은색": "silver",
"청금색": "blue-gold",
"청은색": "blue-silver",
"흑청색": "black-blue",
"흑적색": "black-red",
"흑녹색": "black-green",
"흑회색": "black-gray",
"흰청색": "white-blue",
"흰적색": "white-red",
"흰녹색": "white-green",
"흰회색": "white-gray",
"청적색": "blue-red",
"청녹색": "blue-green",
"청회색": "blue-gray",
"적녹색": "red-green",
"적회색": "red-gray",
"녹회색": "green-gray",
"기타": "other",
"기타색상": "other",
"특별색상": "special",
"무채색": "neutral",
"메탈릭": "metallic",
"": "pearl",
"무광": "matte",
"광택": "glossy",
}
# Типы кузова
BODY_TYPE_MAP = {
"세단": "SEDAN",
"SUV": "SUV",
"SUV ": "SUV",
"해치백": "HATCHBACK",
"왜건": "WAGON",
"컨버터블": "CONVERTIBLE",
"쿠페": "COUPE",
"픽업": "PICKUP",
"": "VAN",
"트럭": "TRUCK",
"버스": "BUS",
"기타": "OTHER",
"승용차": "SEDAN",
"승합차": "VAN",
"화물차": "TRUCK",
"특장차": "SPECIAL",
"경차": "COMPACT",
"중형차": "MID_SIZE",
"대형차": "FULL_SIZE",
"스포츠카": "SPORTS",
"클래식카": "CLASSIC",
"전기차": "ELECTRIC",
"하이브리드": "HYBRID",
}
# Определение body_type по модели
MODEL_BODY_TYPE_MAP: dict[str, str] = {
# SUV / кроссоверы
"투싼": "SUV", "싼타페": "SUV", "팰리세이드": "SUV", "코나": "SUV", "베뉴": "SUV",
"크레타": "SUV", "넥쏘": "SUV", "아이오닉5": "SUV", "아이오닉6": "SEDAN",
"셀토스": "SUV", "스포티지": "SUV", "쏘렌토": "SUV", "모하비": "SUV",
"니로": "SUV", "EV6": "SUV", "EV9": "SUV",
"티볼리": "SUV", "코란도": "SUV", "렉스턴": "SUV", "토레스": "SUV", "액티언": "SUV",
"GV70": "SUV", "GV80": "SUV", "GV60": "SUV",
"X1": "SUV", "X2": "SUV", "X3": "SUV", "X4": "SUV", "X5": "SUV", "X6": "SUV", "X7": "SUV", "iX": "SUV", "iX3": "SUV",
"Q2": "SUV", "Q3": "SUV", "Q4": "SUV", "Q5": "SUV", "Q7": "SUV", "Q8": "SUV",
"GLA": "SUV", "GLB": "SUV", "GLC": "SUV", "GLE": "SUV", "GLS": "SUV",
"카이엔": "SUV", "마칸": "SUV",
"레인지로버": "SUV", "디스커버리": "SUV", "디펜더": "SUV",
"래브4": "SUV", "하이랜더": "SUV", "FJ 크루져": "SUV",
"CR-V": "SUV", "HR-V": "SUV",
"티구안": "SUV", "투아렉": "SUV", "아틀라스": "SUV", "ID.4": "SUV",
"트레일블레이저": "SUV", "이쿼녹스": "SUV", "트래버스": "SUV", "트랙스": "SUV", "캡티바": "SUV", "올란도": "SUV",
"XC40": "SUV", "XC60": "SUV", "XC90": "SUV", "V40": "HATCHBACK", "V60": "STATION_WAGON", "V90": "STATION_WAGON",
"F-PACE": "SUV", "E-PACE": "SUV", "I-PACE": "SUV",
"모델 Y": "SUV", "Model Y": "SUV", "모델 X": "SUV", "Model X": "SUV",
"RX": "SUV", "NX": "SUV", "UX": "SUV", "LX": "SUV",
"QX50": "SUV", "QX60": "SUV", "QX80": "SUV",
"QM3": "SUV", "QM6": "SUV",
"레니게이드": "SUV", "컴패스": "SUV", "체로키": "SUV", "랭글러": "SUV", "글래디에이터": "PICKUP",
"에비에이터": "SUV", "노틸러스": "SUV", "코세어": "SUV",
"마칸": "SUV", "카이엔": "SUV",
"르반떼": "SUV", "그레칼레": "SUV",
"Z4": "CONVERTIBLE",
# 세단
"쏘나타": "SEDAN", "아반떼": "SEDAN", "그랜저": "SEDAN", "K5": "SEDAN",
"K8": "SEDAN", "K9": "SEDAN", "K3": "SEDAN", "K7": "SEDAN",
"G70": "SEDAN", "G80": "SEDAN", "G90": "SEDAN",
"1시리즈": "HATCHBACK",
"2시리즈 그란쿠페": "SEDAN", "2시리즈 액티브투어러": "MINIVAN",
"3시리즈": "SEDAN", "3시리즈 GT": "SEDAN",
"5시리즈": "SEDAN", "7시리즈": "SEDAN", "i4": "SEDAN", "i7": "SEDAN",
"A1": "HATCHBACK", "A3": "SEDAN", "A4": "SEDAN", "A6": "SEDAN", "A7": "SEDAN", "A8": "SEDAN",
"A-클래스": "SEDAN", "CLA": "SEDAN",
"C-클래스": "SEDAN", "E-클래스": "SEDAN", "S-클래스": "SEDAN",
"캠리": "SEDAN", "아발론": "SEDAN", "프리우스": "SEDAN",
"어코드": "SEDAN", "시빅": "SEDAN",
"파사트": "SEDAN", "제타": "SEDAN", "아테온": "SEDAN",
"말리부": "SEDAN", "크루즈": "SEDAN", "임팔라": "SEDAN",
"S60": "SEDAN", "S90": "SEDAN",
"IS": "SEDAN", "ES": "SEDAN", "GS": "SEDAN", "LS": "SEDAN",
"Q50": "SEDAN", "Q70": "SEDAN",
"모델 3": "SEDAN", "Model 3": "SEDAN", "모델 S": "SEDAN", "Model S": "SEDAN",
"SM6": "SEDAN", "SM5": "SEDAN",
# 해치백
"스파크": "HATCHBACK", "모닝": "HATCHBACK", "레이": "HATCHBACK",
"i30": "HATCHBACK", "벨로스터": "HATCHBACK",
"골프": "HATCHBACK", "폴로": "HATCHBACK", "ID.3": "HATCHBACK",
"캐스퍼": "HATCHBACK",
# 미니밴/밴
"카니발": "MINIVAN", "스타리아": "MINIVAN", "스타렉스": "MINIVAN",
"V-클래스": "MINIVAN", "비토": "VAN",
"시에나": "MINIVAN", "알파드": "MINIVAN",
# 트럭/픽업
"포터": "PICKUP", "봉고": "PICKUP", "마이티": "TRUCK",
"타코마": "PICKUP", "터프": "PICKUP",
"콜로라도": "PICKUP", "실버라도": "PICKUP",
# 쿠페
"4시리즈": "COUPE", "8시리즈": "COUPE", "2시리즈": "COUPE",
"A5": "COUPE", "TT": "COUPE",
"RC": "COUPE", "LC": "COUPE",
}
# Regex для парсинга объёма двигателя из Badge
ENGINE_VOLUME_RE = re.compile(r"(\d+\.\d+)\s*(?:T|터보)?")
# Regex для европейских кодов: "530i", "C200", "A220", "GLC300", "740d", "M340i", "S63", "120i", "E220d" и т.д.
@@ -848,9 +480,9 @@ class EncarMapper:
class EncarScraper:
DEFAULT_PAGE_SIZE = 1000 # API поддерживает до 1000
def __init__(self, settings: Settings | None = None) -> None:
def __init__(self, settings: Settings | None = None, persistence: PersistenceService | None = None) -> None:
self.settings = settings or Settings()
self.persistence = PersistenceService(self.settings)
self.persistence = persistence or PersistenceService(self.settings)
self.car_mapper = EncarMapper()
self._batch_pool: urllib3.HTTPSConnectionPool | None = None

View File

@@ -0,0 +1,680 @@
# Словари переводов корейского текста русский / английский.
# Используется в encar.py для маппинга данных Encar API.
# Топливо
FUEL_TYPE_MAP = {
"가솔린": "petrol",
"gasoline": "petrol",
"디젤": "diesel",
"diesel": "diesel",
"전기": "electric",
"electric": "electric",
"하이브리드": "hybrid",
"hybrid": "hybrid",
"lpg": "lpg",
"LPG": "lpg",
"LPG(일반인 구입)": "lpg",
"LPG(장애인 구입)": "lpg",
"가솔린+전기": "hybrid",
"디젤+전기": "hybrid",
"CNG": "cng",
}
# КПП
GEARBOX_MAP_KO = {
"오토": "AT",
"자동": "AT",
"수동": "MT",
"cvt": "CVT",
"CVT": "CVT",
}
# Тип продажи
SELL_TYPE_MAP = {
"일반": "STOCK",
"경매": "AUCTION",
"입찰": "TENDER",
}
# Город → страна
CITY_TO_COUNTRY = {
"서울": "KR", "경기": "KR", "인천": "KR", "부산": "KR", "대구": "KR",
"광주": "KR", "대전": "KR", "울산": "KR", "세종": "KR", "강원": "KR",
"충북": "KR", "충남": "KR", "전북": "KR", "전남": "KR", "경북": "KR",
"경남": "KR", "제주": "KR",
}
# Бренды: корейский → русский
BRAND_TRANSLATIONS = {
"현대": "Хёндэ",
"기아": "Киа",
"쉐보레(GM대우)": "Шевроле",
"벤츠": "Мерседес-Бенц",
"BMW": "BMW",
"아우디": "Ауди",
"폭스바겐": "Фольксваген",
"토요타": "Тойота",
"도요타": "Тойота",
"닛산": "Ниссан",
"혼다": "Хонда",
"포르쉐": "Порше",
"랜드로버": "Ленд Ровер",
"재규어": "Ягуар",
"볼보": "Вольво",
"푸조": "Пежо",
"시트로엥": "Ситроен",
"르노": "Рено",
"미니": "Мини",
"링컨": "Линкольн",
"캐딜락": "Кадиллак",
"크라이슬러": "Крайслер",
"지프": "Джип",
"포드": "Форд",
"테슬라": "Тесла",
"폴크스바겐": "Фольксваген",
"스코다": "Шкода",
"세아트": "SEAT",
"오펠": "Опель",
"피아트": "Фиат",
"알파로메오": "Альфа Ромео",
"마세라티": "Мазерати",
"람보르기니": "Ламборгини",
"페라리": "Феррари",
"벤틀리": "Бентли",
"롤스로이스": "Роллс-Ройс",
"애스턴마틴": "Астон Мартин",
"맥라렌": "Макларен",
"부가티": "Бугатти",
"코닉세그": "Кёнигсегг",
"파가니": "Пагани",
"헤네시": "Hennessey",
"쉘비": "Shelby",
"다임러": "Daimler",
"마이바흐": "Майбах",
"스마트": "Смарт",
"DS": "DS",
"렉서스": "Лексус",
"인피니티": "Инфинити",
"아큐라": "Акура",
"스바루": "Субару",
"미쓰비시": "Мицубиси",
"마쓰다": "Мазда",
"이스즈": "Исузу",
"다이하츠": "Daihatsu",
"스즈키": "Сузуки",
"KG모빌리티(쌍용)": "KG Mobility",
"르노코리아(삼성)": "Рено Корея",
"한국GM": "GM Korea",
"쌍용": "СсангЙонг",
"대우": "Дэу",
"제네시스": "Генезис",
"르노삼성": "Рено Самсунг",
"로터스": "Лотус",
"리비안": "Ривиан",
"루시드": "Люсид",
"리막": "Римак",
"알파 로메오": "Альфа Ромео",
"마쯔다": "Мазда",
"어큐라": "Акура",
"다이하쯔": "Дайхатсу",
# --- Новые бренды (с сайта Encar) ---
"쉐보레": "Шевроле",
"시트로엥/DS": "Ситроен/DS",
"미쯔비시": "Мицубиси",
"미쯔오까": "Мицуока",
"동풍소콘": "Донгфенг Сокон",
"북기은상": "BAIC",
"신위안": "Skywell",
"이네오스": "Инеос",
"지리": "Джили",
"머큐리": "Меркурий",
"사이언": "Scion",
"뷰익": "Бьюик",
"새턴": "Saturn",
"올즈모빌": "Oldsmobile",
"폰티악": "Понтиак",
"포톤": "Foton",
"MG로버": "MG Rover",
"닷지": "Додж",
"험머": "Хаммер",
"폴스타": "Полстар",
"사브": "Сааб",
"기타 제조사": "Другие",
"기타 수입차": "Другие импортные",
}
# Бренды: английский → алиасы (корейский + русский)
BRAND_ENGLISH_ALIASES: dict[str, list[str]] = {
"hyundai": ["현대", "Хёндэ"],
"kia": ["기아", "Киа"],
"genesis": ["제네시스", "Генезис"],
"chevrolet": ["쉐보레(GM대우)", "쉐보레", "Шевроле"],
"mercedes": ["벤츠", "Мерседес-Бенц"],
"mercedes-benz": ["벤츠", "Мерседес-Бенц"],
"bmw": ["BMW"],
"audi": ["아우디", "Ауди"],
"volkswagen": ["폭스바겐", "폴크스바겐", "Фольксваген"],
"toyota": ["토요타", "도요타", "Тойота"],
"nissan": ["닛산", "Ниссан"],
"honda": ["혼다", "Хонда"],
"porsche": ["포르쉐", "Порше"],
"land rover": ["랜드로버", "Ленд Ровер"],
"jaguar": ["재규어", "Ягуар"],
"volvo": ["볼보", "Вольво"],
"peugeot": ["푸조", "Пежо"],
"citroen": ["시트로엥", "Ситроен", "시트로엥/DS", "Ситроен/DS"],
"renault": ["르노", "Рено"],
"mini": ["미니", "Мини"],
"lincoln": ["링컨", "Линкольн"],
"cadillac": ["캐딜락", "Кадиллак"],
"chrysler": ["크라이슬러", "Крайслер"],
"jeep": ["지프", "Джип"],
"ford": ["포드", "Форд"],
"tesla": ["테슬라", "Тесла"],
"lexus": ["렉서스", "Лексус"],
"infiniti": ["인피니티", "Инфинити"],
"acura": ["아큐라", "어큐라", "Акура"],
"subaru": ["스바루", "Субару"],
"mitsubishi": ["미쓰비시", "미쯔비시", "Мицубиси"],
"mazda": ["마쓰다", "마쯔다", "Мазда"],
"suzuki": ["스즈키", "Сузуки"],
"ferrari": ["페라리", "Феррари"],
"lamborghini": ["람보르기니", "Ламборгини"],
"maserati": ["마세라티", "Мазерати"],
"bentley": ["벤틀리", "Бентли"],
"rolls-royce": ["롤스로이스", "Роллс-Ройс"],
"aston martin": ["애스턴마틴", "Астон Мартин"],
"mclaren": ["맥라렌", "Макларен"],
"ssangyong": ["쌍용", "СсангЙонг", "KG모빌리티(쌍용)", "KG Mobility"],
"kg mobility": ["KG모빌리티(쌍용)", "KG Mobility", "쌍용", "СсангЙонг"],
"daewoo": ["대우", "Дэу"],
"dodge": ["닷지", "Додж"],
"hummer": ["험머", "Хаммер"],
"gmc": ["GMC"],
"polestar": ["폴스타", "Полстар"],
"ds": ["DS", "시트로엥/DS", "Ситроен/DS"],
"renault korea": ["르노코리아(삼성)", "Рено Корея", "르노삼성", "Рено Самсунг"],
"saab": ["사브", "Сааб"],
"lotus": ["로터스", "Лотус"],
"alfa romeo": ["알파로메오", "Альфа Ромео", "알파 로메오"],
"fiat": ["피아트", "Фиат"],
"smart": ["스마트", "Смарт"],
"skoda": ["스코다", "Шкода"],
"daihatsu": ["다이하츠", "다이하쯔", "Дайхатсу"],
"koenigsegg": ["코닉세그", "Кёнигсегг"],
"pagani": ["파가니", "Пагани"],
"bugatti": ["부가티", "Бугатти"],
"maybach": ["마이바흐", "Майбах"],
"rivian": ["리비안", "Ривиан"],
"lucid": ["루시드", "Люсид"],
"rimac": ["리막", "Римак"],
"byd": ["BYD"],
"geely": ["지리", "Джили"],
"dongfeng": ["동풍소콘", "Донгфенг Сокон"],
"baic": ["북기은상", "BAIC"],
"skywell": ["신위안", "Skywell"],
"ineos": ["이네오스", "Инеос"],
"mercury": ["머큐리", "Меркурий"],
"scion": ["사이언", "Scion"],
"buick": ["뷰익", "Бьюик"],
"saturn": ["새턴", "Saturn"],
"oldsmobile": ["올즈모빌", "Oldsmobile"],
"pontiac": ["폰티악", "Понтиак"],
"foton": ["포톤", "Foton"],
"mg rover": ["MG로버", "MG Rover"],
"mitsuoka": ["미쯔오까", "Мицуока"],
"other": ["기타 제조사", "기타 수입차", "Другие", "Другие импортные"],
}
def expand_allowed_brands(brands: set[str] | None) -> set[str] | None:
"""Расширяет набор английских брендов всеми известными alias'ами (корейский + русский)."""
if not brands:
return brands
expanded = set(brands)
for brand in brands:
aliases = BRAND_ENGLISH_ALIASES.get(brand.lower(), [])
for alias in aliases:
expanded.add(alias.lower())
return expanded
# Модели / комплектации: корейский → русский
MODEL_TRANSLATIONS = {
# --- Топливо / тип двигателя ---
"가솔린": "бензин",
"디젤": "дизель",
"하이브리드": "гибрид",
"전기": "электро",
"터보": "турбо",
"일렉트릭": "электрик",
"일렉트리파이드": "электрифайд",
"블루텍": "BlueTEC",
"블루모션": "BlueMotion",
# --- Кузов / конструкция ---
"쿠페": "купе",
"세단": "седан",
"왜건": "универсал",
"웨건": "универсал",
"해치백": "хэтчбек",
"컨버터블": "кабриолет",
"카브리올레": "кабриолет",
"크로스오버": "кроссовер",
"리무진": "лимузин",
"하이리무진": "хай-лимузин",
"하이리무진(특장업체)": "хай-лимузин(спецтехника)",
"스포트백": "спортбэк",
"스파이더": "спайдер",
"": "вэн",
"픽업": "пикап",
"캠핑카": "кемпер",
# --- КПП / привод ---
"오토": "автомат",
"수동": "механика",
"후륜": "задний привод",
"전륜": "передний привод",
"사륜": "полный привод",
"4모션": "4Motion",
# --- Трим / комплектация ---
"프레스티지": "престиж",
"시그니처": "сигнатур",
"럭셔리": "люкс",
"스포츠": "спорт",
"롱레인지": "дальний запас хода",
"스탠다드": "стандарт",
"플러스": "плюс",
"리미티드": "лимитед",
"스페셜": "спешл",
"모던": "модерн",
"익스클루시브": "эксклюзив",
"캘리그래피": "каллиграфи",
"노블레스": "ноблесс",
"트림": "трим",
"프리미엄": "премиум",
"프리미어": "премьер",
"플래티넘": "платинум",
"플래티넘Ⅱ": "платинум II",
"인스퍼레이션": "инспирейшн",
"트렌디": "тренди",
"다이나믹": "динамик",
"프라임": "прайм",
"초이스": "чойс",
"르블랑": "Ле Блан",
"프레지던트": "президент",
"인스크립션": "инскрипшн",
"볼드": "болд",
"밸류": "вэлью",
"퓨어": "пьюр",
"엑셀런스": "экселленс",
"얼티메이트": "алтимейт",
"에센셜": "эссеншл",
"슈프림": "суприм",
"에어": "эйр",
"어반": "урбан",
"": "топ",
"코어": "кор",
"엘리트": "элит",
"셀렉션": "селекшн",
"브라이트": "брайт",
"클래식": "классик",
"고급형": "высшая комплектация",
"최고급형": "максимальная комплектация",
"기본형": "базовая комплектация",
"디럭스": "делюкс",
"디럭스팩": "делюкс-пакет",
"마이스터": "мастер",
"마스터": "мастер",
"마스터즈": "мастерс",
"어메이징": "амейзинг",
"파워": "пауэр",
"리저브": "резерв",
"퍼펙트": "перфект",
"리얼": "реал",
"울트라": "ультра",
"세이프티": "сейфти",
"퍼포먼스": "перформанс",
"인텔리전트": "интеллиджент",
"컬렉션": "коллекшн",
"브릴리언트": "бриллиант",
"어드밴티지": "адвантейдж",
"파이니스트": "файнест",
"런치팩": "ланч-пак",
"페이버드": "фейворд",
"엘레강스": "элеганс",
"하이": "хай",
"블랙라벨": "Блэк Лейбл",
"플럭스": "флюкс",
"테크": "тек",
"프로": "про",
"베스트": "бест",
"라이즈": "райз",
"액티브": "актив",
"": "лонг",
"패키지": "пакет",
"에디션": "эдишн",
"디자인": "дизайн",
"라운지": "лаунж",
"아트": "арт",
"": "поп",
"조이": "джой",
"레드라인": "редлайн",
"스타일": "стайл",
"블랙": "блэк",
"라이트": "лайт",
"온라인": "онлайн",
"론지튜드": "лонгитюд",
"오버랜드": "оверленд",
"컴페티션": "компетишн",
"올스페이스": "оллспейс",
"스타": "стар",
# --- Поколение / номер ---
"1세대": "1 поколение",
"2세대": "2 поколение",
"3세대": "3 поколение",
"4세대": "4 поколение",
"5세대": "5 поколение",
"6세대": "6 поколение",
"7세대": "7 поколение",
"8세대": "8 поколение",
"10세대": "10 поколение",
"": "",
"": "нью",
"넥스트": "некст",
"": "олл",
"그란": "гран",
"그랜드": "гранд",
# --- Посадочных мест ---
"2인승": "2-местный",
"3인승": "3-местный",
"5인승": "5-местный",
"6인승": "6-местный",
"7인승": "7-местный",
"9인승": "9-местный",
"11인승": "11-местный",
"12인승": "12-местный",
# --- Двери ---
"4도어": "4-дверный",
"5도어": "5-дверный",
# --- Модели Hyundai ---
"팰리세이드": "Палисейд",
"싼타페": "Санта Фе",
"투싼": "Туксон",
"아반떼": "Аванте",
"쏘나타": "Соната",
"그랜저": "Грэнджер",
"아이오닉5": "Ioniq 5",
"아이오닉6": "Ioniq 6",
"아이오닉": "Ioniq",
"넥쏘": "Нексо",
"벨로스터": "Велостер",
"코나": "Кона",
"캐스퍼": "Каспер",
"베뉴": "Венью",
"스타렉스": "Старекс",
"스타리아": "Стариа",
"에쿠스(신형)": "Экус",
"엑센트(신형)": "Акцент",
"아슬란": "Аслан",
"베라크루즈": "Веракруз",
# --- Модели Kia ---
"카니발": "Карнивал",
"쏘렌토": "Соренто",
"스포티지": "Спортейдж",
"모닝": "Морнинг",
"뉴모닝": "нью Морнинг",
"레이": "Рэй",
"셀토스": "Селтос",
"니로": "Ниро",
"스팅어": "Стингер",
"쏘울": "Соул",
"모하비": "Мохаве",
"프라이드": "Прайд",
"스토닉": "Стоник",
# --- Модели Chevrolet/GM ---
"스파크": "Спарк",
"말리부": "Малибу",
"트랙스": "Тракс",
"크루즈": "Круз",
"올란도": "Орландо",
"트레일블레이저": "Трейлблейзер",
"콜로라도": "Колорадо",
"트래버스": "Траверс",
"캡티바": "Каптива",
"이쿼녹스": "Эквинокс",
"아베오": "Авео",
"볼트": "Вольт",
"임팔라": "Импала",
"맥스크루즈": "МаксКруз",
# --- Модели Genesis ---
"제네시스": "Генезис",
# --- Модели иностранных авто (транслитерация) ---
"익스플로러": "Эксплорер",
"머스탱": "Мустанг",
"카이엔": "Каен",
"파나메라": "Панамера",
"마칸": "Макан",
"타이칸": "Тайкан",
"박스터": "Бокстер",
"카레라": "Каррера",
"티구안": "Тигуан",
"골프": "Гольф",
"제타": "Джетта",
"투아렉": "Туарег",
"파사트": "Пассат",
"아테온": "Артеон",
"콰트로": "кваттро",
"아방가르드": "Авангард",
"쿠퍼": "Купер",
"컨트리맨": "Кантримен",
"클럽맨": "Клабмен",
"디스커버리": "Дискавери",
"레인지로버": "Рейндж Ровер",
"이보크": "Эвок",
"벨라": "Велар",
"디펜더": "Дефендер",
"랭글러": "Рэнглер",
"체로키": "Чероки",
"체로키(KL)": "Чероки (KL)",
"체로키(WL)": "Чероки (WL)",
"레니게이드": "Ренегат",
"컴패스": "Компас",
"루비콘": "Рубикон",
"에비에이터": "Авиатор",
"에스컬레이드": "Эскалейд",
"컨티넨탈": "Континенталь",
"르반떼": "Леванте",
"기블리": "Гибли",
"알티마": "Альтима",
"오딧세이": "Одиссей",
"어코드": "Аккорд",
"캠리": "Камри",
"시에나": "Сиенна",
"프리우스": "Приус",
"크로스컨트리": "Кросс Кантри",
"타스만": "Тасман",
# --- Служебные / категории ---
"(세부등급": "(подуровень",
"없음)": "отсутствует)",
"(렌터카)": "(прокат)",
"(렌터카용)": "(для проката)",
"LPG(렌터카용)": "LPG(для проката)",
"LPI(렌터카용)": "LPI(для проката)",
"(택시형)": "(такси)",
"택시형": "такси",
"어린이보호차": "детский транспорт",
"카고": "карго",
"모델": "модель",
"스마트": "Смарт",
"기아": "Киа",
"마이바흐": "Майбах",
"레인지": "Рейндж",
}
# Цвета: корейский → английский
COLOR_TRANSLATIONS = {
"흰색": "white",
"검정색": "black",
"회색": "gray",
"은색": "silver",
"진회색": "dark gray",
"밝은 회색": "light gray",
"청색": "blue",
"파란색": "blue",
"진청색": "dark blue",
"밝은 청색": "light blue",
"녹색": "green",
"초록색": "green",
"진녹색": "dark green",
"밝은 녹색": "light green",
"적색": "red",
"빨간색": "red",
"진적색": "dark red",
"밝은 적색": "light red",
"주황색": "orange",
"노란색": "yellow",
"갈색": "brown",
"베이지색": "beige",
"아이보리": "ivory",
"크림색": "cream",
"보라색": "purple",
"분홍색": "pink",
"금색": "gold",
"청금색": "blue-gold",
"청은색": "blue-silver",
"흑청색": "black-blue",
"흑적색": "black-red",
"흑녹색": "black-green",
"흑회색": "black-gray",
"흰청색": "white-blue",
"흰적색": "white-red",
"흰녹색": "white-green",
"흰회색": "white-gray",
"청적색": "blue-red",
"청녹색": "blue-green",
"청회색": "blue-gray",
"적녹색": "red-green",
"적회색": "red-gray",
"녹회색": "green-gray",
"기타": "other",
"기타색상": "other",
"특별색상": "special",
"무채색": "neutral",
"메탈릭": "metallic",
"": "pearl",
"무광": "matte",
"광택": "glossy",
}
# Тип кузова: корейский → enum
BODY_TYPE_MAP = {
"세단": "SEDAN",
"SUV": "SUV",
"SUV ": "SUV",
"해치백": "HATCHBACK",
"왜건": "WAGON",
"컨버터블": "CONVERTIBLE",
"쿠페": "COUPE",
"픽업": "PICKUP",
"": "VAN",
"트럭": "TRUCK",
"버스": "BUS",
"기타": "OTHER",
"승용차": "SEDAN",
"승합차": "VAN",
"화물차": "TRUCK",
"특장차": "SPECIAL",
"경차": "COMPACT",
"중형차": "MID_SIZE",
"대형차": "FULL_SIZE",
"스포츠카": "SPORTS",
"클래식카": "CLASSIC",
"전기차": "ELECTRIC",
"하이브리드": "HYBRID",
}
# Определение body_type по модели (fallback)
MODEL_BODY_TYPE_MAP: dict[str, str] = {
# SUV / кроссоверы
"투싼": "SUV", "싼타페": "SUV", "팰리세이드": "SUV", "코나": "SUV", "베뉴": "SUV",
"크레타": "SUV", "넥쏘": "SUV", "아이오닉5": "SUV", "아이오닉6": "SEDAN",
"셀토스": "SUV", "스포티지": "SUV", "쏘렌토": "SUV", "모하비": "SUV",
"니로": "SUV", "EV6": "SUV", "EV9": "SUV",
"티볼리": "SUV", "코란도": "SUV", "렉스턴": "SUV", "토레스": "SUV", "액티언": "SUV",
"GV70": "SUV", "GV80": "SUV", "GV60": "SUV",
"X1": "SUV", "X2": "SUV", "X3": "SUV", "X4": "SUV", "X5": "SUV", "X6": "SUV", "X7": "SUV", "iX": "SUV", "iX3": "SUV",
"Q2": "SUV", "Q3": "SUV", "Q4": "SUV", "Q5": "SUV", "Q7": "SUV", "Q8": "SUV",
"GLA": "SUV", "GLB": "SUV", "GLC": "SUV", "GLE": "SUV", "GLS": "SUV",
"카이엔": "SUV", "마칸": "SUV",
"레인지로버": "SUV", "디스커버리": "SUV", "디펜더": "SUV",
"래브4": "SUV", "하이랜더": "SUV", "FJ 크루져": "SUV",
"CR-V": "SUV", "HR-V": "SUV",
"티구안": "SUV", "투아렉": "SUV", "아틀라스": "SUV", "ID.4": "SUV",
"트레일블레이저": "SUV", "이쿼녹스": "SUV", "트래버스": "SUV", "트랙스": "SUV", "캡티바": "SUV", "올란도": "SUV",
"XC40": "SUV", "XC60": "SUV", "XC90": "SUV", "V40": "HATCHBACK", "V60": "STATION_WAGON", "V90": "STATION_WAGON",
"F-PACE": "SUV", "E-PACE": "SUV", "I-PACE": "SUV",
"모델 Y": "SUV", "Model Y": "SUV", "모델 X": "SUV", "Model X": "SUV",
"RX": "SUV", "NX": "SUV", "UX": "SUV", "LX": "SUV",
"QX50": "SUV", "QX60": "SUV", "QX80": "SUV",
"QM3": "SUV", "QM6": "SUV",
"레니게이드": "SUV", "컴패스": "SUV", "체로키": "SUV", "랭글러": "SUV", "글래디에이터": "PICKUP",
"에비에이터": "SUV", "노틸러스": "SUV", "코세어": "SUV",
"르반떼": "SUV", "그레칼레": "SUV",
"Z4": "CONVERTIBLE",
# 세단
"쏘나타": "SEDAN", "아반떼": "SEDAN", "그랜저": "SEDAN", "K5": "SEDAN",
"K8": "SEDAN", "K9": "SEDAN", "K3": "SEDAN", "K7": "SEDAN",
"G70": "SEDAN", "G80": "SEDAN", "G90": "SEDAN",
"1시리즈": "HATCHBACK",
"2시리즈 그란쿠페": "SEDAN", "2시리즈 액티브투어러": "MINIVAN",
"3시리즈": "SEDAN", "3시리즈 GT": "SEDAN",
"5시리즈": "SEDAN", "7시리즈": "SEDAN", "i4": "SEDAN", "i7": "SEDAN",
"A1": "HATCHBACK", "A3": "SEDAN", "A4": "SEDAN", "A6": "SEDAN", "A7": "SEDAN", "A8": "SEDAN",
"A-클래스": "SEDAN", "CLA": "SEDAN",
"C-클래스": "SEDAN", "E-클래스": "SEDAN", "S-클래스": "SEDAN",
"캠리": "SEDAN", "아발론": "SEDAN", "프리우스": "SEDAN",
"어코드": "SEDAN", "시빅": "SEDAN",
"파사트": "SEDAN", "제타": "SEDAN", "아테온": "SEDAN",
"말리부": "SEDAN", "크루즈": "SEDAN", "임팔라": "SEDAN",
"S60": "SEDAN", "S90": "SEDAN",
"IS": "SEDAN", "ES": "SEDAN", "GS": "SEDAN", "LS": "SEDAN",
"Q50": "SEDAN", "Q70": "SEDAN",
"모델 3": "SEDAN", "Model 3": "SEDAN", "모델 S": "SEDAN", "Model S": "SEDAN",
"SM6": "SEDAN", "SM5": "SEDAN",
# 해치백
"스파크": "HATCHBACK", "모닝": "HATCHBACK", "레이": "HATCHBACK",
"i30": "HATCHBACK", "벨로스터": "HATCHBACK",
"골프": "HATCHBACK", "폴로": "HATCHBACK", "ID.3": "HATCHBACK",
"캐스퍼": "HATCHBACK",
# 미니밴/밴
"카니발": "MINIVAN", "스타리아": "MINIVAN", "스타렉스": "MINIVAN",
"V-클래스": "MINIVAN", "비토": "VAN",
"시에나": "MINIVAN", "알파드": "MINIVAN",
# 트럭/픽업
"포터": "PICKUP", "봉고": "PICKUP", "마이티": "TRUCK",
"타코마": "PICKUP", "터프": "PICKUP",
"콜로라도": "PICKUP", "실버라도": "PICKUP",
# 쿠페
"4시리즈": "COUPE", "8시리즈": "COUPE", "2시리즈": "COUPE",
"A5": "COUPE", "TT": "COUPE",
"RC": "COUPE", "LC": "COUPE",
}

View File

@@ -40,17 +40,9 @@ celery_app.conf.update(
},
result_expires=86400,
worker_hijack_root_logger=False,
beat_schedule={
"periodic-encar-sync": {
"task": "encar_scraper.worker.tasks.encar_sync_listing_task",
"schedule": settings.celery.encar_beat_interval_minutes * 60.0,
"args": (),
"kwargs": {
"car_type": "all",
},
"options": {"queue": "encar"},
},
},
# Следующий запуск планирует сама задача в finally через apply_async(countdown=...),
# чтобы час отсчитывался от завершения прогона, а не от старта.
beat_schedule={},
task_routes={
"encar_scraper.worker.tasks.*": {"queue": "encar"},
},

View File

@@ -5,6 +5,7 @@ from threading import Event, Thread
import uuid
from celery import shared_task
from celery.signals import worker_process_init
from redis import Redis
from ..core.config import Settings
@@ -20,6 +21,13 @@ _persistence: PersistenceService | None = None
_redis: Redis | None = None
@worker_process_init.connect
def _reset_globals_after_fork(**kwargs):
global _persistence, _redis
_persistence = None
_redis = None
def _sync_listing_lock_ttl_seconds() -> int:
settings = Settings()
return max(settings.celery.task_time_limit + 120, 300)
@@ -147,6 +155,7 @@ def encar_sync_listing_task(
heartbeat_stop: Event | None = None
heartbeat_thread: Thread | None = None
run_id: int | None = None
try:
heartbeat_stop, heartbeat_thread = _start_lock_heartbeat(
@@ -162,6 +171,9 @@ def encar_sync_listing_task(
effective_limit = limit or rc.sync.limit
effective_lane = lane or rc.sync.lane or "encar"
# --- Фиксируем начало sync_run ---
run_id = persistence.start_sync_run(effective_lane)
car_type_map = {"all": "A", "domestic": "Y", "import": "N"}
filters = EncarFilters(
car_type=car_type_map.get(car_type, "A"),
@@ -180,7 +192,7 @@ def encar_sync_listing_task(
set(rc.filters.exclude_brands) if rc.filters.exclude_brands else None
)
scraper = EncarScraper()
scraper = EncarScraper(persistence=persistence)
result = scraper.sync_listing(
limit=effective_limit,
filters=filters,
@@ -191,6 +203,16 @@ def encar_sync_listing_task(
probe_all_photos=rc.sync.probe_all_photos,
)
# --- Запись результата в sync_runs ---
persistence.finish_sync_run(
run_id,
status="success",
ids_fetched=result.get("items_collected", 0),
cars_upserted=result.get("cars_synced", 0),
cars_failed=result.get("cars_failed", 0),
images_upserted=0,
)
summary = {
"task_id": task_id,
"source": "encar",
@@ -208,6 +230,19 @@ def encar_sync_listing_task(
except Exception as exc:
logger.error("encar_sync_listing_task failed: %s", exc, exc_info=True)
try:
if run_id is not None:
persistence.finish_sync_run(
run_id,
status="failed",
ids_fetched=0,
cars_upserted=0,
cars_failed=0,
images_upserted=0,
error_summary=str(exc)[:500],
)
except Exception:
logger.warning("Failed to record sync_run failure", exc_info=True)
raise self.retry(exc=exc)
finally:
if heartbeat_stop is not None:
@@ -216,6 +251,20 @@ def encar_sync_listing_task(
heartbeat_thread.join(timeout=max(1.0, min(5.0, lock_ttl / 10)))
if lock_acquired:
_release_lock_if_owner(redis_client, ENCAR_SYNC_LOCK_KEY, owner_token)
try:
interval_minutes = Settings().celery.encar_beat_interval_minutes
countdown = max(60, int(interval_minutes) * 60)
self.apply_async(
kwargs={"car_type": car_type},
countdown=countdown,
queue="encar",
)
logger.info(
"Next encar_sync_listing_task scheduled in %d minutes",
countdown // 60,
)
except Exception:
logger.warning("Failed to schedule next sync run", exc_info=True)
@shared_task(
@@ -231,7 +280,7 @@ def encar_sync_vehicle_task(self, vehicle_url: str, lane: str = "encar"):
persistence.create_tables()
try:
scraper = EncarScraper()
scraper = EncarScraper(persistence=persistence)
result = scraper.sync_vehicle(vehicle_url, lane=lane)
logger.info("encar_sync_vehicle_task completed: %s", vehicle_url)
return {