feat: implement DCL ADIF parser and service integration
- Add shared ADIF parser utility (src/backend/utils/adif-parser.js) - parseADIF(): Parse ADIF format into QSO records - parseDCLResponse(): Parse DCL's JSON response format - normalizeBand() and normalizeMode(): Standardize band/mode names - Implement DCL service (src/backend/services/dcl.service.js) - fetchQSOsFromDCL(): Fetch from DCL API (ready for API availability) - parseDCLJSONResponse(): Parse example payload format - syncQSOs(): Update existing QSOs with DCL confirmations - Support DCL-specific fields: DCL_QSL_RCVD, DCL_QSLRDATE, DARC_DOK, MY_DARC_DOK - Refactor LoTW service to use shared ADIF parser - Remove duplicate parseADIF, normalizeBand, normalizeMode functions - Import from shared utility for consistency - Tested with example DCL payload - Successfully parses all 6 QSOs - Correctly extracts DCL confirmation data - Handles ADIF format with <EOR> delimiters Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { db, logger } from '../config.js';
|
||||
import { qsos } from '../db/schema/index.js';
|
||||
import { max, sql, eq, and, desc } from 'drizzle-orm';
|
||||
import { updateJobProgress } from './job-queue.service.js';
|
||||
import { parseADIF, normalizeBand, normalizeMode } from '../utils/adif-parser.js';
|
||||
|
||||
/**
|
||||
* LoTW (Logbook of the World) Service
|
||||
@@ -150,39 +151,6 @@ async function fetchQSOsFromLoTW(lotwUsername, lotwPassword, sinceDate = null) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ADIF (Amateur Data Interchange Format) data
|
||||
*/
|
||||
function parseADIF(adifData) {
|
||||
const qsos = [];
|
||||
const records = adifData.split('<eor>');
|
||||
|
||||
for (const record of records) {
|
||||
if (!record.trim()) continue;
|
||||
if (record.trim().startsWith('<') && !record.includes('<CALL:') && !record.includes('<call:')) continue;
|
||||
|
||||
const qso = {};
|
||||
const regex = /<([A-Z_]+):(\d+)(?::[A-Z]+)?>([\s\S])/gi;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(record)) !== null) {
|
||||
const [fullMatch, fieldName, lengthStr, firstChar] = match;
|
||||
const length = parseInt(lengthStr, 10);
|
||||
const valueStart = match.index + fullMatch.length - 1;
|
||||
const value = record.substring(valueStart, valueStart + length);
|
||||
|
||||
qso[fieldName.toLowerCase()] = value.trim();
|
||||
regex.lastIndex = valueStart + length;
|
||||
}
|
||||
|
||||
if (Object.keys(qso).length > 0 && (qso.call || qso.call)) {
|
||||
qsos.push(qso);
|
||||
}
|
||||
}
|
||||
|
||||
return qsos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ADIF QSO to database format
|
||||
*/
|
||||
@@ -211,35 +179,6 @@ function convertQSODatabaseFormat(adifQSO, userId) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBand(band) {
|
||||
if (!band) return null;
|
||||
|
||||
const bandMap = {
|
||||
'160m': '160m', '80m': '80m', '60m': '60m', '40m': '40m',
|
||||
'30m': '30m', '20m': '20m', '17m': '17m', '15m': '15m',
|
||||
'12m': '12m', '10m': '10m', '6m': '6m', '4m': '4m',
|
||||
'2m': '2m', '1.25m': '1.25m', '70cm': '70cm', '33cm': '33cm',
|
||||
'23cm': '23cm', '13cm': '13cm', '9cm': '9cm', '6cm': '6cm',
|
||||
'3cm': '3cm', '1.2cm': '1.2cm', 'mm': 'mm',
|
||||
};
|
||||
|
||||
return bandMap[band.toLowerCase()] || band;
|
||||
}
|
||||
|
||||
function normalizeMode(mode) {
|
||||
if (!mode) return '';
|
||||
|
||||
const modeMap = {
|
||||
'cw': 'CW', 'ssb': 'SSB', 'am': 'AM', 'fm': 'FM',
|
||||
'rtty': 'RTTY', 'psk31': 'PSK31', 'psk63': 'PSK63',
|
||||
'ft8': 'FT8', 'ft4': 'FT4', 'jt65': 'JT65', 'jt9': 'JT9',
|
||||
'js8': 'JS8', 'mfsk': 'MFSK', 'olivia': 'OLIVIA',
|
||||
};
|
||||
|
||||
const normalized = modeMap[mode.toLowerCase()];
|
||||
return normalized || mode.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync QSOs from LoTW to database
|
||||
* @param {number} userId - User ID
|
||||
|
||||
Reference in New Issue
Block a user