pliceclogs-og/sheets.js
type-two e5b8504bc6 Import pliceclogs as-is — the original Discogs seller extension
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>
2026-07-24 13:47:09 +10:00

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;
}