diff --git a/api/main.py b/api/main.py index 83781f6..75acbb8 100644 --- a/api/main.py +++ b/api/main.py @@ -38,8 +38,20 @@ def health(): return {"status": "ok"} +def _parse_terms(raw: str) -> list[str]: + """Split a comma-separated keyword string into non-empty, stripped terms.""" + return [t.strip() for t in raw.split(",") if t.strip()] + + @app.get("/api/search") -def search(q: str = "", max_price: float = 0, min_price: float = 0, pages: int = 1): +def search( + q: str = "", + max_price: float = 0, + min_price: float = 0, + pages: int = 1, + must_include: str = "", # comma-separated; applied client-side only + must_exclude: str = "", # comma-separated; forwarded to eBay AND applied client-side +): if not q.strip(): return {"listings": [], "trust_scores": {}, "sellers": {}, "market_price": None} @@ -47,6 +59,8 @@ def search(q: str = "", max_price: float = 0, min_price: float = 0, pages: int = max_price=max_price if max_price > 0 else None, min_price=min_price if min_price > 0 else None, pages=max(1, pages), + must_include=_parse_terms(must_include), + must_exclude=_parse_terms(must_exclude), ) # Each adapter gets its own Store (SQLite connection) — required for thread safety. diff --git a/app/platforms/__init__.py b/app/platforms/__init__.py index f9162ee..69b1fae 100644 --- a/app/platforms/__init__.py +++ b/app/platforms/__init__.py @@ -12,7 +12,9 @@ class SearchFilters: min_price: Optional[float] = None condition: Optional[list[str]] = field(default_factory=list) location_radius_km: Optional[int] = None - pages: int = 1 # number of result pages to fetch (48 listings/page) + pages: int = 1 # number of result pages to fetch (48 listings/page) + must_include: list[str] = field(default_factory=list) # client-side title filter + must_exclude: list[str] = field(default_factory=list) # forwarded to eBay -term AND client-side class PlatformAdapter(ABC): diff --git a/app/platforms/ebay/scraper.py b/app/platforms/ebay/scraper.py index 50e33ed..d493805 100644 --- a/app/platforms/ebay/scraper.py +++ b/app/platforms/ebay/scraper.py @@ -302,6 +302,12 @@ class ScrapedEbayAdapter(PlatformAdapter): if codes: base_params["LH_ItemCondition"] = "|".join(codes) + # Append negative keywords to the eBay query — eBay supports "-term" in _nkw natively. + # This reduces junk results at the source and improves market comp quality. + if filters.must_exclude: + excludes = " ".join(f"-{t.strip()}" for t in filters.must_exclude if t.strip()) + base_params["_nkw"] = f"{base_params['_nkw']} {excludes}" + pages = max(1, filters.pages) page_params = [{**base_params, "_pgn": str(p)} for p in range(1, pages + 1)] diff --git a/web/src/stores/search.ts b/web/src/stores/search.ts index 3ab385a..40d903d 100644 --- a/web/src/stores/search.ts +++ b/web/src/stores/search.ts @@ -60,7 +60,9 @@ export interface SearchFilters { hideNewAccounts?: boolean hideSuspiciousPrice?: boolean hideDuplicatePhotos?: boolean - pages?: number // number of eBay result pages to fetch (48 listings/page, default 1) + pages?: number // number of eBay result pages to fetch (48 listings/page, default 1) + mustInclude?: string // comma-separated; client-side title filter + mustExclude?: string // comma-separated; forwarded to eBay -term AND client-side } // ── Store ──────────────────────────────────────────────────────────────────── @@ -86,6 +88,8 @@ export const useSearchStore = defineStore('search', () => { if (filters.maxPrice != null) params.set('max_price', String(filters.maxPrice)) if (filters.minPrice != null) params.set('min_price', String(filters.minPrice)) if (filters.pages != null && filters.pages > 1) params.set('pages', String(filters.pages)) + if (filters.mustInclude?.trim()) params.set('must_include', filters.mustInclude.trim()) + if (filters.mustExclude?.trim()) params.set('must_exclude', filters.mustExclude.trim()) const res = await fetch(`/api/search?${params}`) if (!res.ok) throw new Error(`Search failed: ${res.status} ${res.statusText}`) diff --git a/web/src/views/SearchView.vue b/web/src/views/SearchView.vue index 1910284..9ca2b34 100644 --- a/web/src/views/SearchView.vue +++ b/web/src/views/SearchView.vue @@ -56,6 +56,35 @@
{{ (filters.pages ?? 1) * 48 }} listings · {{ (filters.pages ?? 1) * 2 }} Playwright calls
+ +