332 lines
10 KiB
JavaScript
332 lines
10 KiB
JavaScript
// api_v1/scanner/scanActivitySingle.js
|
|
const processActivityFile = require('./processActivityFile'); // parser FIT/GPX/TCX
|
|
const { sha256 } = require('./utils');
|
|
const { log } = require('./logger');
|
|
|
|
// Se vuoi forzare il ricalcolo anche di file invariati:
|
|
// ACTIVITY_FORCE_REPARSE=1 node ...
|
|
const FORCE_REPARSE = process.env.ACTIVITY_FORCE_REPARSE === '1';
|
|
|
|
// Normalizza location come per le foto
|
|
function normCountryCode(value) {
|
|
if (!value) return undefined;
|
|
const s = String(value).trim().toUpperCase();
|
|
return s.length ? s : undefined;
|
|
}
|
|
|
|
function normalizeLocation(location) {
|
|
if (!location) return null;
|
|
|
|
const countryCode = normCountryCode(
|
|
location.country_code ||
|
|
location.countryCode ||
|
|
location.isoCountryCode
|
|
);
|
|
|
|
const countyCode =
|
|
location.county_code ||
|
|
location.countyCode ||
|
|
undefined;
|
|
|
|
return {
|
|
...location,
|
|
country: location.country || undefined,
|
|
country_code: countryCode,
|
|
county: location.county || undefined,
|
|
county_code: countyCode,
|
|
region: location.region || undefined,
|
|
city: location.city || undefined,
|
|
postcode: location.postcode || undefined,
|
|
address: location.address || undefined,
|
|
timezone: location.timezone || undefined,
|
|
time: location.time || undefined,
|
|
};
|
|
}
|
|
|
|
function serializeJson(value, fallback = null) {
|
|
if (value == null) return fallback;
|
|
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
async function scanActivitySingle(db, user, cartella, f, newActivities, prefix = '') {
|
|
// const fileName = f.name;
|
|
const fileName = f.file_name ?? f.name ?? f.relPath ?? 'unknown';
|
|
const st = f.stat;
|
|
|
|
// ---------------------------------------------------------
|
|
// ID e fastHash PRIMA del parsing pesante
|
|
// Deve essere identico a processActivityFile:
|
|
// sha256(`${userName}/${cartella}/${fileRelPath}`)
|
|
// ---------------------------------------------------------
|
|
const id = sha256(`${user}/${cartella}/${f.relPath}`);
|
|
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
|
|
|
|
// ---------------------------------------------------------
|
|
// HARD DELETED → non reimportare attività cancellate
|
|
// ---------------------------------------------------------
|
|
const hardDeleted = await db('deleted_hard_activities')
|
|
.where({ id })
|
|
.first();
|
|
|
|
if (hardDeleted) {
|
|
log(`${prefix} ♻️ Activity restored: ${fileName}`);
|
|
|
|
await db('deleted_hard_activities')
|
|
.where({ id })
|
|
.del();
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// Recupero precedente usando id deterministico
|
|
// ---------------------------------------------------------
|
|
let prev = await db('activities')
|
|
.select('id', 'size_bytes', 'last_modified_millis', 'fast_hash', 'path')
|
|
.where({ id })
|
|
.first();
|
|
|
|
// ---------------------------------------------------------
|
|
// PATH CAMBIATO = tratta come nuovo/modificato
|
|
// ---------------------------------------------------------
|
|
if (prev && prev.path !== f.path) {
|
|
log(`${prefix} 🟢 Nuova/Modificata activity (path changed): ${fileName}`);
|
|
prev = null;
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// IDENTICO → SKIP IMMEDIATO, senza parsing FIT/GPX/TCX
|
|
// ---------------------------------------------------------
|
|
if (prev && prev.fast_hash === fastHash && !FORCE_REPARSE) {
|
|
log(`${prefix} ⏭️ Activity invariata, skip parser: ${fileName}`);
|
|
|
|
// Aggiorno solo dati leggeri. Non tocco date_modified_millis,
|
|
// così non genero changes inutili nel progressive sync.
|
|
await db('activities')
|
|
.where({ id })
|
|
.update({
|
|
path: f.path,
|
|
size_bytes: st.size,
|
|
last_modified_millis: st.mtimeMs,
|
|
last_scan_millis: Date.now(),
|
|
fast_hash: fastHash,
|
|
updated_at: new Date().toISOString(),
|
|
});
|
|
|
|
return null;
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// Solo qui facciamo parsing completo FIT/GPX/TCX
|
|
// ---------------------------------------------------------
|
|
const meta = await processActivityFile(
|
|
user,
|
|
cartella,
|
|
f.relPath,
|
|
f.absPath,
|
|
f.ext,
|
|
st
|
|
);
|
|
|
|
meta.path = f.path;
|
|
meta.location = normalizeLocation(meta.location);
|
|
|
|
const trackPreviewJson = serializeJson(meta.track_preview || [], null);
|
|
const tagsJson = serializeJson(meta.tags || [], null);
|
|
const diagnosticsJson = serializeJson(meta.diagnostics || null, null);
|
|
const locationJson = serializeJson(meta.location || null, null);
|
|
|
|
// ---------------------------------------------------------
|
|
// NUOVA ATTIVITÀ
|
|
// ---------------------------------------------------------
|
|
if (!prev) {
|
|
log(`${prefix} 🟢 Nuova activity: ${fileName}`);
|
|
|
|
await db('activities').insert({
|
|
id: meta.id,
|
|
user,
|
|
cartella,
|
|
|
|
file_name: meta.file_name,
|
|
file_extension: meta.file_extension,
|
|
format: meta.format,
|
|
display_name: meta.display_name,
|
|
route_name: meta.route_name,
|
|
title: meta.title,
|
|
description: meta.description,
|
|
notes: meta.notes,
|
|
sport: meta.sport,
|
|
activity_type: meta.activity_type,
|
|
device_manufacturer: meta.device_manufacturer,
|
|
device_model: meta.device_model,
|
|
source_app: meta.source_app,
|
|
|
|
start_time_millis: meta.start_time_millis,
|
|
end_time_millis: meta.end_time_millis,
|
|
duration_millis: meta.duration_millis,
|
|
|
|
distance_meters: meta.distance_meters,
|
|
elevation_gain_meters: meta.elevation_gain_meters,
|
|
elevation_loss_meters: meta.elevation_loss_meters,
|
|
|
|
min_elevation_meters: meta.min_elevation_meters,
|
|
max_elevation_meters: meta.max_elevation_meters,
|
|
|
|
file_distance_meters: meta.file_distance_meters,
|
|
file_elevation_gain_meters: meta.file_elevation_gain_meters,
|
|
|
|
computed_distance_meters: meta.computed_distance_meters,
|
|
computed_elevation_gain_meters: meta.computed_elevation_gain_meters,
|
|
|
|
average_speed_mps: meta.average_speed_mps,
|
|
max_speed_mps: meta.max_speed_mps,
|
|
average_pace_sec_per_km: meta.average_pace_sec_per_km,
|
|
|
|
average_heart_rate: meta.average_heart_rate,
|
|
max_heart_rate: meta.max_heart_rate,
|
|
average_cadence: meta.average_cadence,
|
|
max_cadence: meta.max_cadence,
|
|
|
|
calories: meta.calories,
|
|
power_avg: meta.power_avg,
|
|
power_max: meta.power_max,
|
|
|
|
point_count: meta.point_count,
|
|
segment_count: meta.segment_count,
|
|
lap_count: meta.lap_count,
|
|
|
|
min_latitude: meta.min_latitude,
|
|
max_latitude: meta.max_latitude,
|
|
min_longitude: meta.min_longitude,
|
|
max_longitude: meta.max_longitude,
|
|
center_latitude: meta.center_latitude,
|
|
center_longitude: meta.center_longitude,
|
|
|
|
size_bytes: meta.size_bytes,
|
|
last_modified_millis: meta.last_modified_millis,
|
|
date_added_millis: meta.date_added_millis,
|
|
date_modified_millis: meta.date_modified_millis,
|
|
last_scan_millis: meta.last_scan_millis,
|
|
|
|
is_favorite: meta.is_favorite,
|
|
is_hidden: meta.is_hidden,
|
|
rating: meta.rating,
|
|
|
|
tags: tagsJson,
|
|
track_preview: trackPreviewJson,
|
|
|
|
scan_status: meta.scan_status,
|
|
scan_error: meta.scan_error,
|
|
path: meta.path,
|
|
|
|
diagnostics: diagnosticsJson,
|
|
location: locationJson,
|
|
|
|
fast_hash: fastHash,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
});
|
|
|
|
newActivities.push(meta);
|
|
return meta;
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// MODIFICATO oppure FORCE_REPARSE
|
|
// ---------------------------------------------------------
|
|
if (FORCE_REPARSE) {
|
|
log(`${prefix} 🔁 Force reparse activity: ${fileName}`);
|
|
} else {
|
|
log(`${prefix} 🟠 Modificato activity: ${fileName}`);
|
|
}
|
|
|
|
await db('activities')
|
|
.where({ id })
|
|
.update({
|
|
file_name: meta.file_name,
|
|
file_extension: meta.file_extension,
|
|
format: meta.format,
|
|
display_name: meta.display_name,
|
|
route_name: meta.route_name,
|
|
title: meta.title,
|
|
description: meta.description,
|
|
notes: meta.notes,
|
|
sport: meta.sport,
|
|
activity_type: meta.activity_type,
|
|
device_manufacturer: meta.device_manufacturer,
|
|
device_model: meta.device_model,
|
|
source_app: meta.source_app,
|
|
|
|
start_time_millis: meta.start_time_millis,
|
|
end_time_millis: meta.end_time_millis,
|
|
duration_millis: meta.duration_millis,
|
|
|
|
distance_meters: meta.distance_meters,
|
|
elevation_gain_meters: meta.elevation_gain_meters,
|
|
elevation_loss_meters: meta.elevation_loss_meters,
|
|
|
|
min_elevation_meters: meta.min_elevation_meters,
|
|
max_elevation_meters: meta.max_elevation_meters,
|
|
|
|
file_distance_meters: meta.file_distance_meters,
|
|
file_elevation_gain_meters: meta.file_elevation_gain_meters,
|
|
|
|
computed_distance_meters: meta.computed_distance_meters,
|
|
computed_elevation_gain_meters: meta.computed_elevation_gain_meters,
|
|
|
|
average_speed_mps: meta.average_speed_mps,
|
|
max_speed_mps: meta.max_speed_mps,
|
|
average_pace_sec_per_km: meta.average_pace_sec_per_km,
|
|
|
|
average_heart_rate: meta.average_heart_rate,
|
|
max_heart_rate: meta.max_heart_rate,
|
|
average_cadence: meta.average_cadence,
|
|
max_cadence: meta.max_cadence,
|
|
|
|
calories: meta.calories,
|
|
power_avg: meta.power_avg,
|
|
power_max: meta.power_max,
|
|
|
|
point_count: meta.point_count,
|
|
segment_count: meta.segment_count,
|
|
lap_count: meta.lap_count,
|
|
|
|
min_latitude: meta.min_latitude,
|
|
max_latitude: meta.max_latitude,
|
|
min_longitude: meta.min_longitude,
|
|
max_longitude: meta.max_longitude,
|
|
center_latitude: meta.center_latitude,
|
|
center_longitude: meta.center_longitude,
|
|
|
|
size_bytes: meta.size_bytes,
|
|
last_modified_millis: meta.last_modified_millis,
|
|
date_added_millis: meta.date_added_millis,
|
|
date_modified_millis: meta.date_modified_millis,
|
|
last_scan_millis: meta.last_scan_millis,
|
|
|
|
is_favorite: meta.is_favorite,
|
|
is_hidden: meta.is_hidden,
|
|
rating: meta.rating,
|
|
|
|
tags: tagsJson,
|
|
track_preview: trackPreviewJson,
|
|
|
|
scan_status: meta.scan_status,
|
|
scan_error: meta.scan_error,
|
|
path: meta.path,
|
|
|
|
diagnostics: diagnosticsJson,
|
|
location: locationJson,
|
|
|
|
fast_hash: fastHash,
|
|
updated_at: new Date().toISOString(),
|
|
});
|
|
|
|
newActivities.push(meta);
|
|
return meta;
|
|
}
|
|
|
|
module.exports = scanActivitySingle;
|