From b426020a765fc127dbfcad6c611665fca8a22801 Mon Sep 17 00:00:00 2001 From: Ian Keane Date: Fri, 12 Jun 2026 09:57:24 -0400 Subject: [PATCH] Add notes to import csv --- app.js | 51 ++++++++++++++++++++++++++++++++++++++++++++------- index.html | 2 ++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index 5e15bb7..dd5c370 100644 --- a/app.js +++ b/app.js @@ -1430,12 +1430,30 @@ function parseCSVLine(line) { } function parseCSV(text) { - const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n').filter(l => l.trim()); - if (lines.length < 2) return []; - const headers = parseCSVLine(lines[0]).map(h => h.trim().toLowerCase().replace(/\s+/g, '_')); - return lines.slice(1) - .map(line => { - const vals = parseCSVLine(line); + text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + // Split into rows respecting quoted multiline fields + const rows = []; + let cur = '', inQ = false; + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + if (ch === '"') { + if (inQ && text[i + 1] === '"') { cur += '""'; i++; } + else { inQ = !inQ; cur += ch; } + } else if (ch === '\n' && !inQ) { + rows.push(cur); cur = ''; + } else { + cur += ch; + } + } + if (cur.trim()) rows.push(cur); + + if (rows.length < 2) return []; + const headers = parseCSVLine(rows[0]).map(h => h.trim().toLowerCase().replace(/\s+/g, '_')); + return rows.slice(1) + .filter(r => r.trim()) + .map(r => { + const vals = parseCSVLine(r); const row = {}; headers.forEach((h, i) => { row[h] = (vals[i] ?? '').trim(); }); return row; @@ -1491,11 +1509,13 @@ async function importCSVRows(rows) { } // Add instrument entry (ignore 409 = already exists for that instrument) + // Capture the returned entry so we can attach an instrument note + let entryId = null; if (instId) { const status = ['callable', 'review', 'to_learn'].includes(row.status?.trim()) ? row.status.trim() : null; const difficulty = ['easy', 'hard'].includes(row.difficulty?.trim()?.toLowerCase()) ? row.difficulty.trim().toLowerCase() : null; try { - await apiFetch(`/tunes/${tuneId}/instruments`, { + const entry = await apiFetch(`/tunes/${tuneId}/instruments`, { method: 'POST', body: JSON.stringify({ instrument_id: instId, @@ -1507,11 +1527,28 @@ async function importCSVRows(rows) { difficulty, }), }); + entryId = entry.id; } catch (e) { if (!e.message.startsWith('409')) throw e; } } + // Tune-level note + const tuneNote = row.tune_note?.trim(); + if (tuneNote) { + await apiFetch(`/tunes/${tuneId}/notes`, { + method: 'POST', body: JSON.stringify({ note: tuneNote }), + }); + } + + // Instrument-level note (only if we just created the entry) + const instNote = row.instrument_note?.trim(); + if (instNote && entryId) { + await apiFetch(`/tunes/${tuneId}/instruments/${entryId}/notes`, { + method: 'POST', body: JSON.stringify({ note: instNote }), + }); + } + results.created++; } catch (e) { results.errors.push({ name: tuneName, error: e.message }); diff --git a/index.html b/index.html index ebbd75d..503ed50 100644 --- a/index.html +++ b/index.html @@ -239,6 +239,8 @@ statuscallable / review / to_learn difficultyeasy / hard termCreated if new + tune_noteOne note on the tune + instrument_noteOne note on the instrument entry

If a tune with the same name already exists a new instrument entry is added to it. Sources, tunings, and musicians are created automatically if they don’t exist.