fix: Volvo parser now correctly decodes VIN with vin-group.action
- Changed VIN search URL from vehicle.action to vin-group.action - Updated selectors for Volvo's specific HTML structure (vinInfoTable) - Added parseVolvoInfoTable() to handle multiple label/value pairs per row - Fixed brand detection to use WMI codes (YV1/YV4 = Volvo) - Updated category parsing to use nav-group1-table - Added LEGACY_VOLVO to architectures using vin-group.action Test VIN YV1MV845BF2240557 now returns: - Brand: Volvo - Model: V40 (13-) - Year: 2015 - Categories: 7 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,17 +29,19 @@ export class VolvoParser extends BaseLegacyParser {
|
||||
readonly brands = ['Volvo', 'Polestar'];
|
||||
|
||||
readonly urlPatterns: LegacyUrlPattern = {
|
||||
vinSearch: '{basePath}/vehicle.action?mode=A0LW0TRTR&lang={lang}&vin={vin}&startup=false',
|
||||
// Volvo uses vin-group.action for VIN decode (similar to Hyundai/Opel)
|
||||
vinSearch: '{basePath}/vin-group.action?mode=A0LW0TRTR&lang={lang}&vin={vin}&openVinDialog=true',
|
||||
vehiclePage: '{basePath}/vehicle.action',
|
||||
categoriesPage: '{basePath}/main-groups.action',
|
||||
categoriesPage: '{basePath}/vin-group.action',
|
||||
subGroupsPage: '{basePath}/sub-groups.action',
|
||||
partsPage: '{basePath}/parts.action',
|
||||
partsPage: '{basePath}/vin-image-board.action',
|
||||
imagePage: '{basePath}/image.action',
|
||||
};
|
||||
|
||||
readonly selectors: LegacySelectors = {
|
||||
vehicleInfo: {
|
||||
container: '#vehicleInfo, .vehicle-data, #vinData',
|
||||
// Volvo uses vinInfoTable inside vinDialog
|
||||
container: '#vinDialog, #vinTabsGeneral, .vinInfoTable',
|
||||
brand: '.brand, #brand',
|
||||
model: '.model, #model, .vehicle-model',
|
||||
year: '.year, #year, .model-year',
|
||||
@@ -49,16 +51,18 @@ export class VolvoParser extends BaseLegacyParser {
|
||||
transmission: '.gearbox, #transmission',
|
||||
colorCode: '.color, #colorCode',
|
||||
productionDate: '.prodDate, #productionDate',
|
||||
dataTable: 'table.vin-info, #vinTable, .vehicle-table',
|
||||
// Volvo vehicle info table
|
||||
dataTable: 'table.vinInfoTable',
|
||||
dataRow: 'tr',
|
||||
dataLabel: 'td.label, th, td:first-child',
|
||||
dataValue: 'td.value, td:last-child',
|
||||
dataLabel: 'td.caption',
|
||||
dataValue: 'td:not(.caption)',
|
||||
},
|
||||
categories: {
|
||||
container: '#mainGroupList, .main-groups, #mgList',
|
||||
item: '.main-group, .mg-item, li.mg',
|
||||
code: '.mg-code, .code',
|
||||
name: '.mg-name, .name, a',
|
||||
// Volvo uses nav-group1-table for main categories
|
||||
container: '#nav-group1-table tbody, #nav-group1-table-container',
|
||||
item: 'tr.tc-data-row',
|
||||
code: '.group1',
|
||||
name: '.group1 a, td.group1 a',
|
||||
icon: 'img.mg-icon',
|
||||
link: 'a[href]',
|
||||
},
|
||||
@@ -110,27 +114,75 @@ export class VolvoParser extends BaseLegacyParser {
|
||||
throw new Error(`VIN not found: ${error}`);
|
||||
}
|
||||
|
||||
const rawData = this.parseDataTable(doc);
|
||||
// Volvo uses vinInfoTable with multiple label/value pairs per row
|
||||
const rawData = this.parseVolvoInfoTable(doc);
|
||||
const brand = this.detectBrand(doc, vin);
|
||||
|
||||
return {
|
||||
brand,
|
||||
model: rawData['model'] || rawData['vehicle'] || '',
|
||||
year: this.extractYear(rawData['year'] || rawData['model_year'] || ''),
|
||||
model: rawData['model'] || rawData['model_yili'] || '',
|
||||
year: this.extractYear(rawData['model_yili'] || rawData['model'] || ''),
|
||||
vin,
|
||||
series: rawData['version'] || rawData['trim'] || rawData['variant'] || undefined,
|
||||
bodyType: rawData['body'] || rawData['body_type'] || undefined,
|
||||
engineCode: rawData['engine'] || rawData['engine_code'] || undefined,
|
||||
engineType: rawData['fuel'] || rawData['fuel_type'] || undefined,
|
||||
engineVolume: rawData['displacement'] || rawData['cc'] || undefined,
|
||||
transmission: rawData['gearbox'] || rawData['transmission'] || undefined,
|
||||
driveType: rawData['drive'] || rawData['drivetrain'] || undefined,
|
||||
colorCode: rawData['color'] || rawData['exterior_color'] || undefined,
|
||||
productionDate: rawData['production'] || rawData['mfg_date'] || undefined,
|
||||
series: rawData['turu'] || rawData['satis_tipi'] || undefined,
|
||||
bodyType: rawData['kaporta_stili'] || rawData['karoseri_tipi_kodu'] || undefined,
|
||||
engineCode: rawData['motor'] || rawData['motor_kodu'] || undefined,
|
||||
engineType: rawData['yakit'] || undefined,
|
||||
engineVolume: undefined,
|
||||
transmission: rawData['sanziman'] || rawData['sanziman_kodu'] || undefined,
|
||||
driveType: undefined,
|
||||
colorCode: rawData['dis_rengi'] || undefined,
|
||||
productionDate: rawData['uretim_haftasi'] || undefined,
|
||||
raw: rawData,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Volvo's vinInfoTable which has multiple label/value pairs per row
|
||||
*/
|
||||
private parseVolvoInfoTable(doc: Document): Record<string, string> {
|
||||
const data: Record<string, string> = {};
|
||||
|
||||
// Find the vinInfoTable
|
||||
const table = this.htmlParser.querySelector(doc, 'table.vinInfoTable');
|
||||
if (!table) return data;
|
||||
|
||||
const rows = this.htmlParser.querySelectorAll(table, 'tr');
|
||||
for (const row of rows) {
|
||||
const cells = this.htmlParser.querySelectorAll(row, 'td');
|
||||
|
||||
// Process pairs of cells (caption, value, caption, value...)
|
||||
for (let i = 0; i < cells.length - 1; i += 2) {
|
||||
const captionCell = cells[i];
|
||||
const valueCell = cells[i + 1];
|
||||
|
||||
if (!captionCell || !valueCell) continue;
|
||||
|
||||
// Check if this is a caption cell
|
||||
const captionClass = this.htmlParser.getAttribute(captionCell, 'class') || '';
|
||||
if (!captionClass.includes('caption')) continue;
|
||||
|
||||
const label = this.htmlParser.getTextContent(captionCell)
|
||||
.toLowerCase()
|
||||
.replace(/[:\s]+/g, '_')
|
||||
.replace(/_+$/, '')
|
||||
.replace(/ı/g, 'i')
|
||||
.replace(/ö/g, 'o')
|
||||
.replace(/ü/g, 'u')
|
||||
.replace(/ş/g, 's')
|
||||
.replace(/ç/g, 'c')
|
||||
.replace(/ğ/g, 'g');
|
||||
|
||||
const value = this.htmlParser.getTextContent(valueCell).trim();
|
||||
|
||||
if (label && value && value !== ' ' && value !== ' ') {
|
||||
data[label] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
parseCategoriesPage(html: string): LegacyCategoryData[] {
|
||||
const doc = this.htmlParser.parseHtml(html);
|
||||
const categories: LegacyCategoryData[] = [];
|
||||
@@ -210,21 +262,30 @@ export class VolvoParser extends BaseLegacyParser {
|
||||
// ==================== PRIVATE METHODS ====================
|
||||
|
||||
private detectBrand(doc: Document, vin: string): string {
|
||||
// Check VIN prefix
|
||||
// Check VIN prefix - most reliable method
|
||||
const wmi = vin.substring(0, 3).toUpperCase();
|
||||
|
||||
// Polestar WMIs
|
||||
// Polestar WMIs (specific to Polestar vehicles)
|
||||
if (['LP7', 'YSM'].includes(wmi)) return 'Polestar';
|
||||
|
||||
// Check page content
|
||||
const html = this.htmlParser.getTextContent(doc).toLowerCase();
|
||||
if (html.includes('polestar')) return 'Polestar';
|
||||
// YV1, YV4 are Volvo WMIs - always return Volvo for these
|
||||
if (['YV1', 'YV4'].includes(wmi)) return 'Volvo';
|
||||
|
||||
// Check page title for brand indication
|
||||
const titleEl = this.htmlParser.querySelector(doc, 'title');
|
||||
const title = titleEl ? this.htmlParser.getTextContent(titleEl).toLowerCase() : '';
|
||||
if (title.includes('polestar') && !title.includes('volvo')) return 'Polestar';
|
||||
|
||||
return 'Volvo';
|
||||
}
|
||||
|
||||
private findCategoryLinks(doc: Document): import('domhandler').Element[] {
|
||||
return this.htmlParser.querySelectorAll(doc, 'a[href*="maingroup"], a[href*="mg="]');
|
||||
// Volvo uses nav-group1-table with tr.tc-data-row items
|
||||
const rows = this.htmlParser.querySelectorAll(doc, '#nav-group1-table tbody tr.tc-data-row');
|
||||
if (rows.length > 0) return rows;
|
||||
|
||||
// Fallback to generic selectors
|
||||
return this.htmlParser.querySelectorAll(doc, 'a[href*="maingroup"], a[href*="mg="], a[href*="group1="]');
|
||||
}
|
||||
|
||||
private findSubGroupLinks(doc: Document): import('domhandler').Element[] {
|
||||
|
||||
@@ -160,9 +160,9 @@ export class PL24LegacyScraperService {
|
||||
vehicleId: cleanVin,
|
||||
catalogPath: config.basePath,
|
||||
baseUrl: this.baseUrl,
|
||||
// Hyundai and Opel use vin-group.action for categories, others use main-groups.action
|
||||
mainGroupsPath: (config.architecture === 'LEGACY_HYUNDAI_KIA' || config.architecture === 'LEGACY_OPEL')
|
||||
? `${config.basePath}/vin-group.action?mode=A0LW0TRTR&lang=${this.language}&vin=${cleanVin}&openVinDialog=false`
|
||||
// Hyundai, Opel, and Volvo use vin-group.action for categories
|
||||
mainGroupsPath: (config.architecture === 'LEGACY_HYUNDAI_KIA' || config.architecture === 'LEGACY_OPEL' || config.architecture === 'LEGACY_VOLVO')
|
||||
? `${config.basePath}/vin-group.action?mode=A0LW0TRTR&lang=${this.language}&vin=${cleanVin}&openVinDialog=true`
|
||||
: `${config.basePath}/main-groups.action`,
|
||||
},
|
||||
categories,
|
||||
|
||||
Reference in New Issue
Block a user