Snapshot of the working tree exactly as it stood, no edits. This is the predecessor
PRICEGOD was rewritten from ("kept intact, untouched" per pricegod/README.md), and it
is still the only place the DYMO scale is actually implemented —
rfid-daemon/index.js:1068-1265: HID discovery, parseScaleReport, one-shot read, and a
streaming /weight + /scale/start|stop session whose JSON shape PRICEGOD's daemon.js
already speaks.
Preserved verbatim on purpose (hence -og), including the known bug: DYMO_PIDS at
index.js:1082 is [0x8003, 0x8004], so it cannot see the bench M25 (0x8009). Fix that
in whatever daemon inherits the scale, not here.
node_modules stays ignored (22M of the 24M tree). No credentials in the import: the
two PEM markers in utils.js/sheets.js only strip headers off a key read from settings,
and mrpadmin / johnking are an SSH and a Postgres username, both key/trust auth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
815 lines
30 KiB
JavaScript
815 lines
30 KiB
JavaScript
// Cache-busting comment - Updated to fix ES6 syntax issues - v2.0
|
|
// Functions from utils.js are available globally
|
|
// import { getServiceAccountToken, getGoogleSettings } from './utils.js';
|
|
|
|
// Base64 URL encoding function for JWT tokens
|
|
function base64UrlEncode(data) {
|
|
if (typeof data === 'string') {
|
|
data = new TextEncoder().encode(data);
|
|
}
|
|
|
|
let base64 = btoa(String.fromCharCode(...data));
|
|
|
|
// Convert to base64url format
|
|
return base64.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/=/g, '');
|
|
}
|
|
|
|
async function updateSheet(releaseId, data, range) {
|
|
const googleSettings = await getGoogleSettings();
|
|
if (!googleSettings || !googleSettings.spreadsheetId) {
|
|
throw new Error('Google settings not configured.');
|
|
}
|
|
|
|
const token = await getServiceAccountToken();
|
|
const searchResponse = await fetch(
|
|
`https://sheets.googleapis.com/v4/spreadsheets/${googleSettings.spreadsheetId}/values/Sheet1!B:B`,
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
const searchData = await searchResponse.json();
|
|
if (!searchData.values) {
|
|
throw new Error('Could not find release ID column in spreadsheet.');
|
|
}
|
|
const rowIndex = searchData.values.findIndex(row => row[0] === releaseId.toString());
|
|
|
|
if (rowIndex === -1) {
|
|
throw new Error('Release not found in spreadsheet!');
|
|
}
|
|
const updateRange = `Sheet1!${range}${rowIndex + 1}`;
|
|
const updateResponse = await fetch(
|
|
`https://sheets.googleapis.com/v4/spreadsheets/${googleSettings.spreadsheetId}/values/${updateRange}?valueInputOption=USER_ENTERED`,
|
|
{
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
values: [[data]]
|
|
})
|
|
}
|
|
);
|
|
|
|
if (!updateResponse.ok) {
|
|
const errorData = await updateResponse.json();
|
|
throw new Error(`Failed to update spreadsheet: ${errorData.error.message}`);
|
|
}
|
|
}
|
|
|
|
// Helper function to get condition abbreviations
|
|
function getConditionAbbrev(condition) {
|
|
const conditionMap = {
|
|
'Generic (GEN)': 'GEN',
|
|
'Mint (M)': 'M',
|
|
'Near Mint (NM)': 'NM',
|
|
'Near Mint (NM or M-)': 'NM',
|
|
'Near Mint Minus (NM-)': 'NM-',
|
|
'Very Good Plus (VG+)': 'VG+',
|
|
'Very Good (VG)': 'VG',
|
|
'Very Good Minus (VG-)': 'VG-',
|
|
'Company Sleeve (C.SL)': 'C.SL',
|
|
'Good Plus Plus (G++)': 'G++',
|
|
'Good Plus (G+)': 'G+',
|
|
'Good (G)': 'G',
|
|
'Fair (F)': 'F',
|
|
'Poor (P)': 'P'
|
|
};
|
|
return conditionMap[condition] || condition;
|
|
}
|
|
|
|
// Helper function to format track artists
|
|
function formatTrackArtists(tracklist) {
|
|
if (!tracklist || !Array.isArray(tracklist)) return '';
|
|
|
|
return tracklist.map(track => {
|
|
let artistsInfo = [];
|
|
|
|
// Handle regular artists
|
|
if (track.artists && track.artists.length > 0) {
|
|
const artists = track.artists.map(artist => {
|
|
const id = artist.id ? `[${artist.id}]` : '';
|
|
return `${artist.name}${id}`;
|
|
}).join(', ');
|
|
artistsInfo.push(`${track.position || ''}: ${artists}`);
|
|
}
|
|
|
|
// Handle extraartists (remixers, producers, etc.)
|
|
if (track.extraartists && track.extraartists.length > 0) {
|
|
const extraArtists = track.extraartists.map(artist => {
|
|
const id = artist.id ? `[${artist.id}]` : '';
|
|
const role = artist.role ? ` - ${artist.role}` : '';
|
|
return `${track.position || ''}. ${artist.name}${id}${role}`;
|
|
}).join('; ');
|
|
artistsInfo.push(extraArtists);
|
|
}
|
|
|
|
return artistsInfo.join(' | ');
|
|
}).filter(item => item).join(' | ');
|
|
}
|
|
|
|
async function addToSpreadsheet(data, showAlert = false) {
|
|
// Add debugging to see what data we're receiving
|
|
// console.log('=== DEBUGGING addToSpreadsheet ===');
|
|
// console.log('Received data object:', data);
|
|
// console.log('Data keys:', Object.keys(data));
|
|
// console.log('data.id:', data.id);
|
|
// console.log('data.artist:', data.artist);
|
|
// console.log('data.title:', data.title);
|
|
// console.log('data.lowPrice:', data.lowPrice);
|
|
// console.log('data.have:', data.have);
|
|
// console.log('data.want:', data.want);
|
|
// console.log('data.tracklist:', data.tracklist);
|
|
// console.log('data.extraartists:', data.extraartists);
|
|
// console.log('=== END DEBUGGING ===');
|
|
|
|
const googleSettings = await getGoogleSettings();
|
|
if (!googleSettings || !googleSettings.spreadsheetId) {
|
|
alert('Google settings not configured.');
|
|
return;
|
|
}
|
|
|
|
const price = data.price?.trim() || '';
|
|
const range = price === '$5' ? 'FIVEDOLLAR' : 'Sheet1';
|
|
|
|
try {
|
|
const token = await getServiceAccountToken();
|
|
|
|
// Prepare all the data variables
|
|
const formattedTimestamp = new Date().toISOString();
|
|
const labelName = data.label || '';
|
|
const formatText = data.formats ?
|
|
`${data.format_quantity || 1}x ${data.formats.map(f => `${f.name} ${f.descriptions?.join(', ') || ''}`).join(', ')}`.trim() : '';
|
|
|
|
// Get current UI values (fallback to data if not available)
|
|
const currentMediaCondition = data.mediaCondition || '';
|
|
const currentSleeveCondition = data.sleeveCondition || '';
|
|
const currentPrice = data.price || '';
|
|
|
|
// Market data
|
|
const marketData = {
|
|
lowPrice: data.lowPrice || '',
|
|
medianPrice: data.medianPrice || '',
|
|
highPrice: data.highPrice || '',
|
|
lastSold: data.lastSold || '',
|
|
labelId: data.labelId || '',
|
|
imageUrl: data.imageUrl || ''
|
|
};
|
|
|
|
// Process complex data
|
|
const tracklistData = data.tracklist ?
|
|
data.tracklist.map(track => `${track.position || ''}: ${track.title || ''}`).join(' | ') : '';
|
|
|
|
const extraArtistsData = data.extraartists ?
|
|
data.extraartists.map(artist => `${artist.name} - ${artist.role}`).join(', ') : '';
|
|
|
|
const videosData = data.videos ?
|
|
data.videos.map(video => video.uri).join(', ') : '';
|
|
|
|
const priceSuggestionsString = data.priceSuggestions ?
|
|
Object.entries(data.priceSuggestions).map(([condition, price]) => `${condition}: ${price}`).join(', ') : '';
|
|
|
|
// Get current column mappings (will use defaults if none saved)
|
|
const mappings = await getCurrentMappings();
|
|
|
|
// Build the complete row dynamically based on mappings
|
|
const row = await buildRowFromMappings(mappings, {
|
|
formattedTimestamp,
|
|
data,
|
|
labelName,
|
|
formatText,
|
|
currentMediaCondition,
|
|
currentSleeveCondition,
|
|
currentPrice,
|
|
marketData,
|
|
tracklistData,
|
|
extraArtistsData,
|
|
videosData,
|
|
priceSuggestionsString
|
|
});
|
|
|
|
const values = [row];
|
|
|
|
const response = await fetch(
|
|
`https://sheets.googleapis.com/v4/spreadsheets/${googleSettings.spreadsheetId}/values/${range}:append?valueInputOption=USER_ENTERED`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ values })
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.error.message}`);
|
|
}
|
|
|
|
if (showAlert) {
|
|
return { success: true, message: 'written to sheet√' };
|
|
}
|
|
return { success: true };
|
|
} catch (error) {
|
|
// console.error('Error adding to spreadsheet:', error);
|
|
if (showAlert) {
|
|
return { success: false, message: `Failed: ${error.message}` };
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Add updateSheetOnly function from popup.js
|
|
async function updateSheetOnly(releaseData) {
|
|
// console.log('=== GOOGLE SHEETS UPDATE STARTED ===');
|
|
// console.log('Release data received:', releaseData);
|
|
|
|
try {
|
|
const googleSettings = await getGoogleSettings();
|
|
// console.log('Google settings retrieved:', googleSettings ? 'Present' : 'Missing');
|
|
|
|
if (!googleSettings || !googleSettings.spreadsheetId || !googleSettings.clientEmail || !googleSettings.privateKey) {
|
|
// console.error('Google settings incomplete:', googleSettings);
|
|
return { success: false, message: 'Google Sheets settings not configured properly' };
|
|
}
|
|
|
|
// console.log('Attempting to update Google Sheet...');
|
|
const result = await updateGoogleSheet(releaseData, googleSettings);
|
|
// console.log('Google Sheets update result:', result);
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
// console.error('Error in updateSheetOnly:', error);
|
|
return { success: false, message: `Google Sheets error: ${error.message}` };
|
|
}
|
|
}
|
|
|
|
async function updateGoogleSheet(releaseData, googleSettings) {
|
|
// console.log('=== UPDATING GOOGLE SHEET ===');
|
|
// console.log('Spreadsheet ID:', googleSettings.spreadsheetId);
|
|
// console.log('Client Email:', googleSettings.clientEmail);
|
|
// console.log('Private Key present:', !!googleSettings.privateKey);
|
|
|
|
try {
|
|
// Get access token using service account
|
|
const accessToken = await getServiceAccountToken();
|
|
// console.log('Access token obtained successfully');
|
|
|
|
// Use the same comprehensive data mapping as addToSpreadsheet
|
|
const data = releaseData;
|
|
|
|
// Prepare all the data variables (same as addToSpreadsheet)
|
|
const formattedTimestamp = data._fixedTimestamp || new Date().toISOString();
|
|
const labelName = data.label || '';
|
|
const formatText = data.formats ?
|
|
`${data.format_quantity || 1}x ${data.formats.map(f => `${f.name} ${f.descriptions?.join(', ') || ''}`).join(', ')}`.trim() : '';
|
|
|
|
// Get current UI values (fallback to data if not available)
|
|
const currentMediaCondition = data.mediaCondition || '';
|
|
const currentSleeveCondition = data.sleeveCondition || '';
|
|
const currentPrice = data.price || '';
|
|
|
|
// Market data
|
|
const marketData = {
|
|
lowPrice: data.lowPrice || '',
|
|
medianPrice: data.medianPrice || '',
|
|
highPrice: data.highPrice || '',
|
|
lastSold: data.lastSold || '',
|
|
labelId: data.labelId || '',
|
|
imageUrl: data.imageUrl || ''
|
|
};
|
|
|
|
// Process complex data
|
|
const tracklistData = data.tracklist ?
|
|
data.tracklist.map(track => `${track.position || ''}: ${track.title || ''}`).join(' | ') : '';
|
|
|
|
const extraArtistsData = data.extraartists ?
|
|
data.extraartists.map(artist => `${artist.name} - ${artist.role}`).join(', ') : '';
|
|
|
|
const videosData = data.videos ?
|
|
data.videos.map(video => video.uri).join(', ') : '';
|
|
|
|
const priceSuggestionsString = data.priceSuggestions ?
|
|
Object.entries(data.priceSuggestions).map(([condition, price]) => `${condition}: ${price}`).join(', ') : '';
|
|
|
|
// Build the complete row with all 38 fields (A through AL) matching exact sheet column order
|
|
const rowData = [
|
|
formattedTimestamp, // A: TIMESTAMP
|
|
data.id || '', // B: REL_ID
|
|
data.artist || '', // C: ARTIST
|
|
data.title || '', // D: TITLE
|
|
data.genre || '', // E: GENRE
|
|
data.style || '', // F: STYLE
|
|
labelName, // G: LABEL
|
|
data.year || '', // H: YEAR
|
|
data.country || '', // I: COUNTRY
|
|
formatText, // J: DESCRIPTION
|
|
getConditionAbbrev(currentMediaCondition), // K: MED
|
|
getConditionAbbrev(currentSleeveCondition), // L: SLV
|
|
currentPrice, // M: PRICE
|
|
data.have || '', // N: HAVE
|
|
data.want || '', // O: WANT
|
|
data.num_for_sale || '', // P: NUM FOR SALE
|
|
marketData.lowPrice || '', // Q: LOWEST
|
|
marketData.medianPrice || '', // R: MEDIAN
|
|
marketData.highPrice || '', // S: HIGHEST
|
|
marketData.lastSold || '', // T: LAST SOLD
|
|
tracklistData, // U: TRACKLIST
|
|
extraArtistsData, // V: EXTRA ARTISTS
|
|
videosData, // W: YOUTUBE
|
|
priceSuggestionsString, // X: SUGGESTED PRICE
|
|
marketData.labelId || '', // Y: LABEL ID
|
|
data.artists?.map(a => a.id).join(', ') || '', // Z: ARTIST IDS
|
|
'', // AA: (empty column)
|
|
marketData.imageUrl || '', // AB: IMAGE
|
|
data.comment || '', // AC: COMMENT
|
|
data.images?.map(img => img.uri).join(', ') || '', // AD: image urls
|
|
data.appleId || '', // AE: APPLE ID
|
|
data.labels?.[0]?.catno || '', // AF: CAT NO
|
|
formatTrackArtists(data.tracklist), // AG: TRACK ARTISTS
|
|
data.collectionFolder || '', // AH: FOLDER
|
|
data.notes || '', // AI: NOTES
|
|
data.companies?.map(c => `${c.name} - ${c.entity_type_name} - ${c.id}`).join(', ') || '', // AJ: companies
|
|
data.identifiers?.map(c => `${c.type} - ${c.value}`).join(', ') || '', // AK: identifiers
|
|
data.estimated_weight || '' // AL: estimated wt
|
|
];
|
|
|
|
// console.log('Row data prepared:', rowData);
|
|
|
|
// Determine the correct sheet based on price
|
|
const price = data.price?.trim() || '';
|
|
const sheetName = price === '$5' ? 'FIVEDOLLAR' : 'Sheet1';
|
|
|
|
// Append the data to the sheet
|
|
const appendUrl = `https://sheets.googleapis.com/v4/spreadsheets/${googleSettings.spreadsheetId}/values/${sheetName}:append?valueInputOption=USER_ENTERED`;
|
|
|
|
const response = await fetch(appendUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
values: [rowData]
|
|
})
|
|
});
|
|
|
|
// console.log('Google Sheets API response status:', response.status);
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
// console.log('Google Sheets update successful:', result);
|
|
return { success: true, message: 'Data successfully added to Google Sheets!' };
|
|
} else {
|
|
const errorText = await response.text();
|
|
// console.error('Google Sheets API error:', response.status, errorText);
|
|
return { success: false, message: `Google Sheets API error: ${response.status}` };
|
|
}
|
|
|
|
} catch (error) {
|
|
// console.error('Error updating Google Sheet:', error);
|
|
return { success: false, message: `Error updating Google Sheet: ${error.message}` };
|
|
}
|
|
}
|
|
|
|
async function createJWT(clientEmail, privateKey, scope) {
|
|
// console.log('Creating JWT token...');
|
|
|
|
const header = {
|
|
alg: 'RS256',
|
|
typ: 'JWT'
|
|
};
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const payload = {
|
|
iss: clientEmail,
|
|
scope: scope || 'https://www.googleapis.com/auth/spreadsheets',
|
|
aud: 'https://oauth2.googleapis.com/token',
|
|
exp: now + 3600,
|
|
iat: now
|
|
};
|
|
|
|
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
|
|
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
|
|
// Import the private key
|
|
const keyData = privateKey.replace(/-----BEGIN PRIVATE KEY-----/, '')
|
|
.replace(/-----END PRIVATE KEY-----/, '')
|
|
.replace(/\s/g, '');
|
|
|
|
const binaryKey = Uint8Array.from(atob(keyData), c => c.charCodeAt(0));
|
|
|
|
const cryptoKey = await crypto.subtle.importKey(
|
|
'pkcs8',
|
|
binaryKey,
|
|
{
|
|
name: 'RSASSA-PKCS1-v1_5',
|
|
hash: 'SHA-256'
|
|
},
|
|
false,
|
|
['sign']
|
|
);
|
|
|
|
const signature = await crypto.subtle.sign(
|
|
'RSASSA-PKCS1-v1_5',
|
|
cryptoKey,
|
|
new TextEncoder().encode(signatureInput)
|
|
);
|
|
|
|
const encodedSignature = base64UrlEncode(new Uint8Array(signature));
|
|
|
|
const jwt = `${signatureInput}.${encodedSignature}`;
|
|
// console.log('JWT token created successfully');
|
|
return jwt;
|
|
}
|
|
|
|
async function getAccessToken(jwtToken) {
|
|
// console.log('Getting access token...');
|
|
|
|
const response = await fetch('https://oauth2.googleapis.com/token', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwtToken}`
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
// console.log('Access token obtained successfully');
|
|
return data.access_token;
|
|
} else {
|
|
const errorText = await response.text();
|
|
// console.error('Error getting access token:', response.status, errorText);
|
|
throw new Error(`Failed to get access token: ${response.status}`);
|
|
}
|
|
}
|
|
|
|
// Function to detect sheet column structure and headers
|
|
async function detectSheetStructure(spreadsheetId, sheetName = 'Sheet1') {
|
|
try {
|
|
const accessToken = await getServiceAccountToken();
|
|
|
|
// First, get the sheet properties to find the actual column count
|
|
const sheetPropsUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}?fields=sheets.properties`;
|
|
const propsResponse = await fetch(sheetPropsUrl, {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!propsResponse.ok) {
|
|
throw new Error(`Failed to get sheet properties: ${propsResponse.status}`);
|
|
}
|
|
|
|
const propsData = await propsResponse.json();
|
|
const sheet = propsData.sheets.find(s => s.properties.title === sheetName) || propsData.sheets[0];
|
|
const columnCount = sheet.properties.gridProperties.columnCount;
|
|
|
|
// Convert column count to letter range (e.g., 37 columns = A:AK)
|
|
const endColumn = getColumnLetter(columnCount);
|
|
const headerRange = `${sheetName}!A1:${endColumn}1`;
|
|
|
|
// Get the header row
|
|
const headerUrl = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(headerRange)}`;
|
|
const headerResponse = await fetch(headerUrl, {
|
|
headers: {
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!headerResponse.ok) {
|
|
throw new Error(`Failed to get sheet headers: ${headerResponse.status}`);
|
|
}
|
|
|
|
const headerData = await headerResponse.json();
|
|
const headers = headerData.values ? headerData.values[0] : [];
|
|
|
|
// Build column structure
|
|
const columns = [];
|
|
for (let i = 0; i < columnCount; i++) {
|
|
const columnLetter = getColumnLetter(i + 1);
|
|
const headerText = headers[i] || '';
|
|
columns.push({
|
|
letter: columnLetter,
|
|
header: headerText,
|
|
index: i
|
|
});
|
|
}
|
|
|
|
return {
|
|
columnCount,
|
|
endColumn,
|
|
columns,
|
|
sheetName: sheet.properties.title
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error('Error detecting sheet structure:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Helper function to convert column index to letter (1=A, 2=B, etc.)
|
|
function getColumnLetter(columnIndex) {
|
|
let result = '';
|
|
while (columnIndex > 0) {
|
|
columnIndex--;
|
|
result = String.fromCharCode(65 + (columnIndex % 26)) + result;
|
|
columnIndex = Math.floor(columnIndex / 26);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Function to auto-detect field mappings based on header text
|
|
function detectFieldMappings(columns) {
|
|
const mappings = {};
|
|
const fieldPatterns = {
|
|
'timestamp': /^(timestamp|time|date)$/i,
|
|
'rel_id': /^(rel_id|release_id|id|discogs_id)$/i,
|
|
'artist': /^(artist|artists)$/i,
|
|
'title': /^(title|album|release)$/i,
|
|
'genre': /^(genre|genres)$/i,
|
|
'style': /^(style|styles)$/i,
|
|
'label': /^(label|labels)$/i,
|
|
'year': /^(year|released|date)$/i,
|
|
'country': /^(country|nation)$/i,
|
|
'description': /^(description|format|desc)$/i,
|
|
'med': /^(med|media|condition|media_condition)$/i,
|
|
'slv': /^(slv|sleeve|sleeve_condition)$/i,
|
|
'price': /^(price|cost|amount)$/i,
|
|
'have': /^(have|quantity|qty)$/i,
|
|
'want': /^(want|wanted)$/i,
|
|
'num_for_sale': /^(num_for_sale|for_sale|available)$/i,
|
|
'lowest': /^(lowest|low|min_price)$/i,
|
|
'median': /^(median|med_price|average)$/i,
|
|
'highest': /^(highest|high|max_price)$/i,
|
|
'last_sold': /^(last_sold|sold|recent)$/i,
|
|
'tracklist': /^(tracklist|tracks|track_list)$/i,
|
|
'extra_artists': /^(extra_artists|featuring|feat)$/i,
|
|
'youtube': /^(youtube|video|videos)$/i,
|
|
'suggested_price': /^(suggested_price|suggestion|recommended)$/i,
|
|
'label_id': /^(label_id|lbl_id)$/i,
|
|
'artist_ids': /^(artist_ids|artist_id)$/i,
|
|
'image': /^(image|img|picture|photo)$/i,
|
|
'comment': /^(comment|comments|notes)$/i,
|
|
'image_urls': /^(image_urls|images|img_urls)$/i,
|
|
'apple_id': /^(apple_id|itunes|apple)$/i,
|
|
'cat_no': /^(cat_no|catalog|catalogue|catno)$/i,
|
|
'track_artists': /^(track_artists|track_artist)$/i,
|
|
'folder': /^(folder|collection|coll)$/i,
|
|
'notes': /^(notes|note|remarks)$/i,
|
|
'companies': /^(companies|company|labels)$/i,
|
|
'identifiers': /^(identifiers|barcode|upc)$/i,
|
|
'estimated_wt': /^(estimated_wt|weight|wt)$/i
|
|
};
|
|
|
|
columns.forEach(column => {
|
|
const header = column.header.trim();
|
|
let matchedField = 'skip';
|
|
|
|
// Try to match header text to field patterns
|
|
for (const [fieldType, pattern] of Object.entries(fieldPatterns)) {
|
|
if (pattern.test(header)) {
|
|
matchedField = fieldType;
|
|
break;
|
|
}
|
|
}
|
|
|
|
mappings[column.letter] = matchedField;
|
|
});
|
|
|
|
return mappings;
|
|
}
|
|
|
|
// Helper function to get current mappings (from popup.js)
|
|
async function getCurrentMappings() {
|
|
return new Promise((resolve) => {
|
|
chrome.storage.local.get(['columnMappings', 'detectedStructure'], (result) => {
|
|
if (result.columnMappings) {
|
|
resolve({
|
|
mappings: result.columnMappings,
|
|
structure: result.detectedStructure || null
|
|
});
|
|
} else {
|
|
// Return default mappings if none saved
|
|
resolve({
|
|
mappings: getDefaultMappings(),
|
|
structure: null
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Default mappings matching the original hardcoded structure
|
|
function getDefaultMappings() {
|
|
return {
|
|
'A': 'timestamp',
|
|
'B': 'rel_id',
|
|
'C': 'artist',
|
|
'D': 'title',
|
|
'E': 'genre',
|
|
'F': 'style',
|
|
'G': 'label',
|
|
'H': 'year',
|
|
'I': 'country',
|
|
'J': 'description',
|
|
'K': 'med',
|
|
'L': 'slv',
|
|
'M': 'price',
|
|
'N': 'have',
|
|
'O': 'want',
|
|
'P': 'num_for_sale',
|
|
'Q': 'lowest',
|
|
'R': 'median',
|
|
'S': 'highest',
|
|
'T': 'last_sold',
|
|
'U': 'tracklist',
|
|
'V': 'extra_artists',
|
|
'W': 'youtube',
|
|
'X': 'suggested_price',
|
|
'Y': 'label_id',
|
|
'Z': 'artist_ids',
|
|
'AA': 'skip',
|
|
'AB': 'image',
|
|
'AC': 'comment',
|
|
'AD': 'image_urls',
|
|
'AE': 'apple_id',
|
|
'AF': 'cat_no',
|
|
'AG': 'track_artists',
|
|
'AH': 'folder',
|
|
'AI': 'notes',
|
|
'AJ': 'companies',
|
|
'AK': 'identifiers',
|
|
'AL': 'estimated_wt'
|
|
};
|
|
}
|
|
|
|
// Build row based on dynamic mappings
|
|
async function buildRowFromMappings(mappingsData, dataContext) {
|
|
const { mappings, structure } = mappingsData;
|
|
const {
|
|
formattedTimestamp,
|
|
data,
|
|
labelName,
|
|
formatText,
|
|
currentMediaCondition,
|
|
currentSleeveCondition,
|
|
currentPrice,
|
|
marketData,
|
|
tracklistData,
|
|
extraArtistsData,
|
|
videosData,
|
|
priceSuggestionsString
|
|
} = dataContext;
|
|
|
|
const row = [];
|
|
|
|
// Use detected structure columns if available, otherwise default A-AL
|
|
const columns = structure ?
|
|
structure.columns.map(c => c.letter) :
|
|
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL'];
|
|
|
|
for (const column of columns) {
|
|
const fieldType = mappings[column] || 'skip';
|
|
let value = '';
|
|
|
|
switch (fieldType) {
|
|
case 'timestamp':
|
|
value = formattedTimestamp;
|
|
break;
|
|
case 'rel_id':
|
|
value = data.id || '';
|
|
break;
|
|
case 'artist':
|
|
value = data.artist || '';
|
|
break;
|
|
case 'title':
|
|
value = data.title || '';
|
|
break;
|
|
case 'genre':
|
|
value = data.genre || '';
|
|
break;
|
|
case 'style':
|
|
value = data.style || '';
|
|
break;
|
|
case 'label':
|
|
value = labelName;
|
|
break;
|
|
case 'year':
|
|
value = data.year || '';
|
|
break;
|
|
case 'country':
|
|
value = data.country || '';
|
|
break;
|
|
case 'description':
|
|
value = formatText;
|
|
break;
|
|
case 'med':
|
|
value = getConditionAbbrev(currentMediaCondition);
|
|
break;
|
|
case 'slv':
|
|
value = getConditionAbbrev(currentSleeveCondition);
|
|
break;
|
|
case 'price':
|
|
value = currentPrice;
|
|
break;
|
|
case 'have':
|
|
value = data.have || '';
|
|
break;
|
|
case 'want':
|
|
value = data.want || '';
|
|
break;
|
|
case 'num_for_sale':
|
|
value = data.num_for_sale || '';
|
|
break;
|
|
case 'lowest':
|
|
value = marketData.lowPrice || '';
|
|
break;
|
|
case 'median':
|
|
value = marketData.medianPrice || '';
|
|
break;
|
|
case 'highest':
|
|
value = marketData.highPrice || '';
|
|
break;
|
|
case 'last_sold':
|
|
value = marketData.lastSold || '';
|
|
break;
|
|
case 'tracklist':
|
|
value = tracklistData;
|
|
break;
|
|
case 'extra_artists':
|
|
value = extraArtistsData;
|
|
break;
|
|
case 'youtube':
|
|
value = videosData;
|
|
break;
|
|
case 'suggested_price':
|
|
value = priceSuggestionsString;
|
|
break;
|
|
case 'label_id':
|
|
value = marketData.labelId || '';
|
|
break;
|
|
case 'artist_ids':
|
|
value = data.artists?.map(a => a.id).join(', ') || '';
|
|
break;
|
|
case 'image':
|
|
value = marketData.imageUrl || '';
|
|
break;
|
|
case 'comment':
|
|
value = data.comment || '';
|
|
break;
|
|
case 'image_urls':
|
|
value = data.images?.map(img => img.uri).join(', ') || '';
|
|
break;
|
|
case 'apple_id':
|
|
value = data.appleId || '';
|
|
break;
|
|
case 'cat_no':
|
|
value = data.labels?.[0]?.catno || '';
|
|
break;
|
|
case 'track_artists':
|
|
value = formatTrackArtists(data.tracklist);
|
|
break;
|
|
case 'folder':
|
|
value = data.collectionFolder || '';
|
|
break;
|
|
case 'notes':
|
|
value = data.notes || '';
|
|
break;
|
|
case 'companies':
|
|
value = data.companies?.map(c => `${c.name} - ${c.entity_type_name} - ${c.id}`).join(', ') || '';
|
|
break;
|
|
case 'identifiers':
|
|
value = data.identifiers?.map(c => `${c.type} - ${c.value}`).join(', ') || '';
|
|
break;
|
|
case 'estimated_wt':
|
|
value = data.estimated_weight || '';
|
|
break;
|
|
case 'actual_weight':
|
|
case 'actualWeight':
|
|
value = data.actual_weight != null ? data.actual_weight : '';
|
|
break;
|
|
case 'skip':
|
|
default:
|
|
value = '';
|
|
break;
|
|
}
|
|
|
|
row.push(value);
|
|
}
|
|
|
|
return row;
|
|
} |