server_photo_s85js/api_v1/scanner/scanActivitySingle.js
2026-08-11 08:45:04 +02:00

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;