Add deduplication and normalize names/URLs

Add product name cleanup and URL normalization across backend, scrapers, and frontend; introduce product/event deduplication. Key changes:
- Clean up SVG/CSS garbage, trailing "X Reviews", mojibake and extra spaces in product names (api, scrapers, JS).
- Normalize URLs (lowercase host, strip tracking/query params, trim trailing slashes) in scrapers and Database and use normalized URL for comparisons/storage.
- Database: use normalized URLs when creating/getting products, prevent duplicate stock events within 1 hour, adjust recent events query to avoid duplicate products.
- Implement deduplication routines in Database: deduplicate_products (by product_id and normalized/clean name) and deduplicate_events (remove duplicate events within same hour).
- API: add POST endpoints /products/deduplicate and /events/deduplicate to trigger deduplication and return stats.
- Frontend: add UI buttons and handlers to call deduplication endpoints and display results.

These changes reduce duplicate product records/events caused by minor URL/name variations and stray CSS/svg artifacts.
This commit is contained in:
2026-03-28 13:31:30 -04:00
parent ad5d179ba3
commit f01bde54a5
5 changed files with 366 additions and 20 deletions
+44 -1
View File
@@ -18,10 +18,16 @@ api_bp = Blueprint('api', __name__)
def clean_product_name(name: str) -> str:
"""Fix common encoding issues in product names"""
"""Fix common encoding issues and garbage in product names"""
if not name:
return name
# Remove SVG/CSS class garbage (pikachu_svg__st1{fill:#7c888f}.review-full-...)
name = re.sub(r'[\w-]*_svg__[\w\s{}:#.;-]+', '', name, flags=re.IGNORECASE)
name = re.sub(r'\.review-full-[\w\s{}:#.;-]+', '', name, flags=re.IGNORECASE)
name = re.sub(r'\{[^}]*\}', '', name) # Remove any remaining {css} blocks
name = re.sub(r'\d+\s*Reviews?$', '', name, flags=re.IGNORECASE) # Remove trailing "X Reviews"
# Fix Pokémon encoding issues
name = re.sub(r'Pok[éÃ\u00c3\u00a9]+mon', 'Pokémon', name, flags=re.IGNORECASE)
name = name.replace('Pokémon', 'Pokémon')
@@ -48,6 +54,9 @@ def clean_product_name(name: str) -> str:
name = name.replace('®', '®')
name = name.replace('Â', '')
# Clean up multiple spaces
name = re.sub(r'\s+', ' ', name)
return name.strip()
@@ -908,6 +917,40 @@ def cleanup_broken_pokemoncenter_urls():
})
@api_bp.route('/products/deduplicate', methods=['POST'])
def deduplicate_products():
"""
Remove duplicate products from the database.
Duplicates are identified by:
- Same product_id and site
- Same name (case-insensitive) and site
"""
db = get_database()
stats = db.deduplicate_products()
return jsonify({
'success': True,
'duplicates_found': stats['duplicates_found'],
'products_removed': stats['products_removed'],
'by_site': stats['by_site']
})
@api_bp.route('/events/deduplicate', methods=['POST'])
def deduplicate_events():
"""
Remove duplicate events from the database.
Duplicates are events for the same product/type within the same hour.
"""
db = get_database()
stats = db.deduplicate_events()
return jsonify({
'success': True,
'events_removed': stats['events_removed']
})
@api_bp.route('/extension/clear', methods=['POST'])
def clear_extension_data():
"""Clear all extension data"""