// Global state let uploadedPhotos = []; let hostedPhotoUrls = []; let currentAnalysis = null; let detectedPhotoTypes = new Set(); // DOM Elements const dropZone = document.getElementById('dropZone'); const photoInput = document.getElementById('photoInput'); const photoGrid = document.getElementById('photoGrid'); const resultsSection = document.getElementById('resultsSection'); const emptyState = document.getElementById('emptyState'); // Event Listeners - Initialize after DOM is ready function initDropZone() { const dz = document.getElementById('dropZone'); const pInput = document.getElementById('photoInput'); if (!dz || !pInput) { console.error('Drop zone elements not found'); return; } // Store references on elements to prevent duplicate initialization if (dz._initialized) { console.log('Drop zone already initialized, skipping'); return; } dz._initialized = true; // Click to browse - use event delegation pattern dz.addEventListener('click', (e) => { // Prevent triggering when clicking on child elements like the spinner if (e.target.closest('#uploadSpinner')) return; if (e.target.closest('.remove-btn')) return; e.preventDefault(); e.stopPropagation(); pInput.click(); }); // Drag over - show visual feedback dz.addEventListener('dragenter', (e) => { e.preventDefault(); e.stopPropagation(); dz.classList.add('drag-over'); }); dz.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); dz.classList.add('drag-over'); }); // Drag leave - remove visual feedback dz.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); // Only remove if we're actually leaving the dropzone, not entering a child if (!dz.contains(e.relatedTarget)) { dz.classList.remove('drag-over'); } }); // Drop - handle files dz.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); dz.classList.remove('drag-over'); const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/')); if (files.length > 0) { addPhotos(files); } else { showToast('Please drop image files only', 'warning'); } }); // File input change pInput.addEventListener('change', (e) => { const files = Array.from(e.target.files); if (files.length > 0) { addPhotos(files); // Reset input so same files can be selected again pInput.value = ''; } }); } function handleDrop(e) { e.preventDefault(); e.stopPropagation(); const dz = document.getElementById('dropZone'); if (dz) dz.classList.remove('drag-over'); const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/')); addPhotos(files); } function handleFileSelect(e) { const files = Array.from(e.target.files); addPhotos(files); } async function addPhotos(files) { if (!files || files.length === 0) return; uploadedPhotos.push(...files); renderPhotoGrid(); updateEmptyState(); // Auto-detect photo types first (uses filename heuristics) setTimeout(() => analyzePhotoTypes(), 100); // Auto-upload to imgbb if available if (localStorage.getItem('imgbb_api_key')) { setTimeout(() => uploadPhotosToImgBB(files), 200); } // Trigger OCR analysis if we have photos and API key if (uploadedPhotos.length > 0 && (localStorage.getItem('openai_api_key') || localStorage.getItem('deepseek_api_key'))) { setTimeout(() => analyzePhotosWithOCR(), 500); } else if (uploadedPhotos.length > 0 && !localStorage.getItem('openai_api_key') && !localStorage.getItem('deepseek_api_key')) { showToast('Add AI API key in Settings for auto-detection', 'warning'); } } async function uploadPhotosToImgBB(files) { const apiKey = localStorage.getItem('imgbb_api_key'); if (!apiKey) { console.log('No imgBB API key configured, skipping upload'); return; } const progressBar = document.getElementById('uploadBar'); const progressContainer = document.getElementById('uploadProgress'); const percentText = document.getElementById('uploadPercent'); progressContainer.classList.remove('hidden'); hostedPhotoUrls = []; // Reset hosted URLs for (let i = 0; i < files.length; i++) { const file = files[i]; const base64 = await fileToBase64(file); const formData = new FormData(); formData.append('image', base64.split(',')[1]); // Remove data:image/*;base64, prefix formData.append('key', apiKey); formData.append('name', `${Date.now()}_${file.name.replace(/[^a-zA-Z0-9.]/g, '_')}`); try { const response = await fetch('https://api.imgbb.com/1/upload', { method: 'POST', body: formData }); const data = await response.json(); if (data.success && data.data) { // Store as object with all relevant URLs from API response hostedPhotoUrls.push({ id: data.data.id, url: data.data.url, displayUrl: data.data.display_url, viewerUrl: data.data.url_viewer, deleteUrl: data.data.delete_url, thumb: data.data.thumb?.url || data.data.url, medium: data.data.medium?.url || data.data.display_url, filename: data.data.image?.filename || file.name, width: data.data.width, height: data.data.height, size: data.data.size, expiration: data.data.expiration }); console.log('Uploaded:', data.data.url); } else { console.error('Upload failed:', data.status, data); } } catch (error) { console.error('Upload failed:', error); } const progress = ((i + 1) / files.length) * 100; progressBar.style.width = `${progress}%`; percentText.textContent = `${Math.round(progress)}%`; } setTimeout(() => { progressContainer.classList.add('hidden'); if (hostedPhotoUrls.length > 0) { showToast(`${hostedPhotoUrls.length} photos uploaded to imgBB`, 'success'); console.log('Hosted URLs:', hostedPhotoUrls.map(u => ({ url: u.url, deleteUrl: u.deleteUrl }))); } }, 500); } async function analyzePhotoTypes() { if (uploadedPhotos.length === 0) return; detectedPhotoTypes.clear(); // Always do basic filename analysis first uploadedPhotos.forEach(file => { const name = file.name.toLowerCase(); if (name.includes('front') || name.includes('cover') || name.includes('front')) detectedPhotoTypes.add('front'); if (name.includes('back') || name.includes('rear')) detectedPhotoTypes.add('back'); if (name.includes('spine')) detectedPhotoTypes.add('spine'); if (name.includes('label') && (name.includes('a') || name.includes('side1') || name.includes('side_1'))) detectedPhotoTypes.add('label_a'); if (name.includes('label') && (name.includes('b') || name.includes('side2') || name.includes('side_2'))) detectedPhotoTypes.add('label_b'); if (name.includes('inner') || name.includes('sleeve')) detectedPhotoTypes.add('inner'); if (name.includes('insert') || name.includes('poster')) detectedPhotoTypes.add('insert'); if (name.includes('hype') || name.includes('sticker')) detectedPhotoTypes.add('hype'); if (name.includes('vinyl') || name.includes('record') || name.includes('disc')) detectedPhotoTypes.add('vinyl'); if (name.includes('corner') || name.includes('edge')) detectedPhotoTypes.add('corners'); if (name.includes('barcode')) detectedPhotoTypes.add('barcode'); if (name.includes('matrix') || name.includes('runout') || name.includes('deadwax')) detectedPhotoTypes.add('deadwax'); }); // Update UI immediately with filename-based detection renderShotList(); // Try AI detection if available const service = getAIService(); if (service && service.apiKey) { showToast('Analyzing photo types with AI...', 'success'); try { // Analyze each photo to determine what shot it is for (let i = 0; i < Math.min(uploadedPhotos.length, 4); i++) { // Limit to first 4 to save API calls try { const result = await identifyPhotoType(uploadedPhotos[i], service); if (result && result.type) { detectedPhotoTypes.add(result.type); } } catch (e) { console.error('Photo type analysis failed for image', i, e); } } renderShotList(); showToast(`Detected ${detectedPhotoTypes.size} shot types`, 'success'); } catch (e) { console.error('AI photo type detection failed:', e); } } } async function identifyPhotoType(imageFile, service) { // Simple heuristic based on filename first const name = imageFile.name.toLowerCase(); // If we can use AI vision, do so if (service && service.apiKey) { try { const base64 = await fileToBase64Clean(imageFile); const messages = [ { role: 'system', content: `You are analyzing a vinyl record photo. Identify which type of shot this is from this list: - front: Front cover/album artwork - back: Back cover/tracklist - spine: Spine with text - label_a: Side A label - label_b: Side B label - deadwax: Deadwax/runout grooves showing matrix numbers (critical for pressing identification) - inner: Inner sleeve - insert: Insert or poster - hype: Hype sticker on shrink - vinyl: Vinyl in raking light showing condition - corners: Close-up of sleeve corners/edges - barcode: Barcode area For deadwax photos, look for: hand-etched matrix numbers, stamped codes, "STERLING", "MASTERED BY", plant symbols, or any alphanumeric codes in the runout groove area. Return ONLY a JSON object: {"type": "one_of_the_above", "confidence": "high|medium|low"}` }, { role: 'user', content: [ { type: 'text', text: 'What type of record photo is this?' }, { type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64}`, detail: 'low' } } ] } ]; const response = await fetch(service.baseUrl || 'https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${service.apiKey}` }, body: JSON.stringify({ model: service.model || 'gpt-4o-mini', messages: messages, max_tokens: 100, temperature: 0.1 }) }); if (response.ok) { const data = await response.json(); const content = data.choices[0].message.content; const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```|([\s\S]*)/); const jsonStr = jsonMatch ? (jsonMatch[1] || jsonMatch[2]) : content; return JSON.parse(jsonStr.trim()); } } catch (e) { console.log('AI photo type detection failed, using filename heuristics'); } } return null; } function fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { resolve(reader.result); // Return full data URL including prefix }; reader.onerror = reject; reader.readAsDataURL(file); }); } // Helper to get clean base64 without data URL prefix function fileToBase64Clean(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const base64 = reader.result.split(',')[1]; resolve(base64); }; reader.onerror = reject; reader.readAsDataURL(file); }); } function getAIService() { const provider = localStorage.getItem('ai_provider') || 'openai'; if (provider === 'deepseek' && window.deepseekService?.isConfigured) { return window.deepseekService; } return window.ocrService; } // Analysis progress state let analysisProgressInterval = null; function updateAnalysisProgress(stage, percent) { const stageText = document.getElementById('analysisStageText'); const percentText = document.getElementById('analysisPercent'); const progressBar = document.getElementById('analysisBar'); if (stageText) stageText.textContent = stage; if (percentText) percentText.textContent = `${percent}%`; if (progressBar) progressBar.style.width = `${percent}%`; } function startAnalysisProgressSimulation() { const stages = [ { stage: 'Preparing images...', target: 15 }, { stage: 'Uploading to AI service...', target: 35 }, { stage: 'Analyzing labels and covers...', target: 60 }, { stage: 'Extracting text with OCR...', target: 80 }, { stage: 'Identifying pressing details...', target: 95 }, { stage: 'Finalizing results...', target: 100 } ]; let currentStage = 0; let currentPercent = 0; updateAnalysisProgress(stages[0].stage, 0); analysisProgressInterval = setInterval(() => { if (currentStage >= stages.length) { clearInterval(analysisProgressInterval); return; } const stage = stages[currentStage]; const increment = Math.random() * 3 + 1; // Random increment between 1-4% currentPercent = Math.min(currentPercent + increment, stage.target); updateAnalysisProgress(stage.stage, Math.floor(currentPercent)); if (currentPercent >= stage.target && currentStage < stages.length - 1) { currentStage++; currentPercent = stage.target; } }, 200); } function stopAnalysisProgress() { if (analysisProgressInterval) { clearInterval(analysisProgressInterval); analysisProgressInterval = null; } updateAnalysisProgress('Complete!', 100); } async function analyzePhotosWithOCR() { const spinner = document.getElementById('uploadSpinner'); const dropZone = document.getElementById('dropZone'); try { spinner.classList.remove('hidden'); dropZone.classList.add('pointer-events-none'); // Start progress simulation startAnalysisProgressSimulation(); // Determine which AI service to use const provider = localStorage.getItem('ai_provider') || 'openai'; const service = getAIService(); // Update API keys if (provider === 'openai') { const apiKey = localStorage.getItem('openai_api_key'); if (!apiKey) throw new Error('OpenAI API key not configured'); window.ocrService.updateApiKey(apiKey); } else { const apiKey = localStorage.getItem('deepseek_api_key'); if (!apiKey) throw new Error('DeepSeek API key not configured'); window.deepseekService.updateApiKey(apiKey); window.deepseekService.updateModel(localStorage.getItem('deepseek_model') || 'deepseek-chat'); } const result = await service.analyzeRecordImages(uploadedPhotos); // Complete the progress bar stopAnalysisProgress(); populateFieldsFromOCR(result); // Try to fetch additional data from Discogs if available if (result.artist && result.title && window.discogsService) { try { const discogsData = await window.discogsService.searchRelease( result.artist, result.title, result.catalogueNumber ); if (discogsData) { populateFieldsFromDiscogs(discogsData); } } catch (e) { console.log('Discogs lookup failed:', e); } } const confidenceMsg = result.confidence === 'high' ? 'Record identified!' : result.confidence === 'medium' ? 'Record found (verify details)' : 'Partial match found'; showToast(confidenceMsg, result.confidence === 'high' ? 'success' : 'warning'); } catch (error) { console.error('OCR Error:', error); if (error.message.includes('API key') || error.message.includes('not configured')) { const provider = localStorage.getItem('ai_provider') || 'openai'; showToast(`Please configure ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'} API key in Settings`, 'error'); } else { showToast(`Analysis failed: ${error.message}`, 'error'); } } finally { stopAnalysisProgress(); // Small delay to show 100% before hiding setTimeout(() => { spinner.classList.add('hidden'); dropZone.classList.remove('pointer-events-none'); // Reset progress for next time updateAnalysisProgress('Initializing...', 0); }, 300); } } function populateFieldsFromDiscogs(discogsData) { if (!discogsData) return; // Update year if not already set or if Discogs has better data const yearInput = document.getElementById('yearInput'); if (discogsData.year && (!yearInput.value || yearInput.value === '[Verify]')) { yearInput.value = discogsData.year; yearInput.classList.add('border-orange-500', 'bg-orange-500/10'); setTimeout(() => { yearInput.classList.remove('border-orange-500', 'bg-orange-500/10'); }, 3000); } // Store additional Discogs data for later use if (discogsData.id) { window.discogsReleaseId = discogsData.id; } // Show Discogs match indicator let panel = document.getElementById('detectedInfoPanel'); if (panel) { const discogsBadge = document.createElement('div'); discogsBadge.className = 'mt-2 pt-2 border-t border-green-500/20'; discogsBadge.innerHTML = `
Matched on Discogs View →
`; panel.appendChild(discogsBadge); feather.replace(); } } function populateFieldsFromOCR(data) { if (!data) { console.error('No OCR data received'); return; } const fields = { 'artistInput': data.artist, 'titleInput': data.title, 'catInput': data.catalogueNumber, 'yearInput': data.year }; let populatedCount = 0; Object.entries(fields).forEach(([fieldId, value]) => { const field = document.getElementById(fieldId); if (field && value && value !== 'null' && value !== 'undefined') { // Only populate if field is empty or user hasn't manually entered if (!field.value || field.dataset.userModified !== 'true') { field.value = value; field.classList.add('border-green-500', 'bg-green-500/10'); setTimeout(() => { field.classList.remove('border-green-500', 'bg-green-500/10'); }, 3000); populatedCount++; } } }); // Store additional data for later use if (data.label) window.detectedLabel = data.label; if (data.country) window.detectedCountry = data.country; if (data.format) window.detectedFormat = data.format; if (data.genre) window.detectedGenre = data.genre; if (data.pressingInfo) window.detectedPressingInfo = data.pressingInfo; if (data.conditionEstimate) window.detectedCondition = data.conditionEstimate; if (data.notes) window.detectedNotes = data.notes; // Store pressing identification data if (data.pressingType) window.detectedPressingType = data.pressingType; if (data.isFirstPress) window.detectedIsFirstPress = data.isFirstPress; if (data.reissueYear) window.detectedReissueYear = data.reissueYear; if (data.originalYear) window.detectedOriginalYear = data.originalYear; // Update UI to show detected info updateDetectedInfoPanel(data); // Scroll to quick details section so user can verify if (populatedCount > 0) { const quickDetailsSection = document.querySelector('.md\\:w-80'); if (quickDetailsSection) { setTimeout(() => { quickDetailsSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100); } } } // Track user modifications to fields document.addEventListener('DOMContentLoaded', () => { ['artistInput', 'titleInput', 'catInput', 'yearInput'].forEach(id => { const field = document.getElementById(id); if (field) { field.addEventListener('input', () => { field.dataset.userModified = 'true'; }); } }); }); function updateDetectedInfoPanel(data) { if (!data) return; // Create or update detected info panel let panel = document.getElementById('detectedInfoPanel'); const parent = document.querySelector('#dropZone')?.parentNode; if (!parent) return; if (!panel) { panel = document.createElement('div'); panel.id = 'detectedInfoPanel'; parent.appendChild(panel); } panel.className = 'mt-4 p-4 bg-green-500/10 border border-green-500/30 rounded-lg'; const infoItems = []; if (data.label && data.label !== 'null') infoItems.push(`Label: ${data.label}`); if (data.country && data.country !== 'null') infoItems.push(`Country: ${data.country}`); if (data.format && data.format !== 'null') infoItems.push(`Format: ${data.format}`); if (data.genre && data.genre !== 'null') infoItems.push(`Genre: ${data.genre}`); if (data.conditionEstimate && data.conditionEstimate !== 'null') infoItems.push(`Est. Condition: ${data.conditionEstimate}`); if (data.pressingInfo && data.pressingInfo !== 'null') infoItems.push(`Matrix: ${data.pressingInfo}`); // Add pressing identification info if (data.isFirstPress !== undefined) { const pressBadge = data.isFirstPress ? 'FIRST PRESS' : data.pressingType === 'reissue' ? 'REISSUE' : data.pressingType === 'repress' ? 'REPRESS' : ''; if (pressBadge) infoItems.push(pressBadge); } if (data.originalYear && data.originalYear !== data.year) { infoItems.push(`Original Year: ${data.originalYear}`); } if (data.reissueYear && data.reissueYear !== data.year) { infoItems.push(`Reissue Year: ${data.reissueYear}`); } const confidenceColor = data.confidence === 'high' ? 'text-green-400' : data.confidence === 'medium' ? 'text-yellow-400' : 'text-orange-400'; panel.innerHTML = `
AI Detected Information (${data.confidence || 'unknown'} confidence)
${infoItems.length > 0 ? `
${infoItems.map(item => `
${item}
`).join('')}
` : '

Limited information detected. Try uploading clearer photos of labels and covers.

'} ${data.notes?.length ? `

Additional notes:

` : ''} `; feather.replace(); } function renderPhotoGrid() { if (uploadedPhotos.length === 0) { photoGrid.classList.add('hidden'); return; } photoGrid.classList.remove('hidden'); photoGrid.innerHTML = uploadedPhotos.map((file, idx) => `
Photo ${idx + 1}
`).join(''); feather.replace(); } function removePhoto(idx) { // Also delete from imgBB if hosted if (hostedPhotoUrls[idx]) { const hosted = hostedPhotoUrls[idx]; if (hosted.deleteUrl) { deleteHostedImage(hosted.deleteUrl); } hostedPhotoUrls.splice(idx, 1); } uploadedPhotos.splice(idx, 1); renderPhotoGrid(); updateEmptyState(); } function updateEmptyState() { if (uploadedPhotos.length > 0) { // Keep empty state visible until generation } } // Mock Discogs API integration for tracklist lookup async function fetchDiscogsData(artist, title, catNo) { // In production, this would call the Discogs API // For demo, return mock data structure return { found: false, message: 'Connect Discogs API for automatic tracklist lookup' }; } // Generate listing analysis async function generateListing() { const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); const catNo = document.getElementById('catInput').value.trim(); const year = document.getElementById('yearInput').value.trim(); const cost = parseFloat(document.getElementById('costInput').value) || 0; const goal = document.getElementById('goalSelect').value; const market = document.getElementById('marketSelect').value; // Validation if (uploadedPhotos.length === 0) { showToast('Please upload at least one photo', 'error'); return; } // Simulate analysis delay dropZone.classList.add('analyzing'); setTimeout(() => { dropZone.classList.remove('analyzing'); performAnalysis({ artist, title, catNo, year, cost, goal, market }); }, 1500); } async function performAnalysis(data) { const { artist, title, catNo, year, cost, goal, market } = data; // Determine currency symbol const currency = market === 'uk' ? '£' : market === 'us' ? 'const container = document.getElementById('titleOptions'); container.innerHTML = titles.map((t, i) => `
${t.chars}/80

${t.text}

${t.style}

`).join(''); } function selectTitle(el, text) { document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); // Update clipboard copy navigator.clipboard.writeText(text); showToast('Title copied to clipboard!', 'success'); } function renderPricingStrategy(bin, strategy, comps, currency, goal) { const container = document.getElementById('pricingStrategy'); const offerSettings = goal === 'max' ? 'Offers: OFF' : `Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`; container.innerHTML = `

Sold Comps by Grade

NM/NM- ${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX ${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+ ${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})

Based on last 90 days sold listings, same pressing. Prices exclude postage.

`; } function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) { const container = document.getElementById('feeFloor'); container.innerHTML = `

Your Cost

${currency}${cost.toFixed(2)}

Est. Fees

${currency}${fees.toFixed(2)}

~16% total

Ship + Pack

${currency}${(shipping + packing).toFixed(2)}

Safe Floor Price

${currency}${safeFloor}

Auto-decline below this

`; } async function renderHTMLDescription(data, titleObj) { const { artist, title, catNo, year } = data; // Use hosted URL if available, otherwise fallback to local object URL let heroImg = ''; let galleryImages = []; if (hostedPhotoUrls.length > 0) { heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url; galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url); } else if (uploadedPhotos.length > 0) { heroImg = URL.createObjectURL(uploadedPhotos[0]); galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1])); } // Use OCR-detected values if available const detectedLabel = window.detectedLabel || '[Verify from photos]'; const detectedCountry = window.detectedCountry || 'UK'; const detectedFormat = window.detectedFormat || 'LP • 33rpm'; const detectedGenre = window.detectedGenre || 'rock'; const detectedCondition = window.detectedCondition || 'VG+/VG+'; const detectedPressingInfo = window.detectedPressingInfo || ''; // Fetch tracklist and detailed info from Discogs if available let tracklistHtml = ''; let pressingDetailsHtml = ''; let provenanceHtml = ''; if (window.discogsReleaseId && window.discogsService?.key) { try { const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId); if (discogsData && discogsData.tracklist) { // Build tracklist HTML const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B'))); if (hasSideBreakdown) { // Group by sides const sides = {}; discogsData.tracklist.forEach(track => { const side = track.position ? track.position.charAt(0) : 'Other'; if (!sides[side]) sides[side] = []; sides[side].push(track); }); tracklistHtml = Object.entries(sides).map(([side, tracks]) => `

Side ${side}

${tracks.map(track => `
${track.position} ${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`).join(''); } else { // Simple list tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`; } // Build pressing/variation details const identifiers = discogsData.identifiers || []; const barcodeInfo = identifiers.find(i => i.type === 'Barcode'); const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout'); const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering'); if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) { pressingDetailsHtml = `

Pressing & Matrix Information

${barcodeInfo ? `

Barcode: ${barcodeInfo.value}

` : ''} ${matrixInfo.map(m => `

${m.type}: ${m.value}${m.description ? ` (${m.description})` : ''}

`).join('')} ${pressingInfo.map(p => `

${p.type}: ${p.value}

`).join('')}
${discogsData.notes ? `

${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}

` : ''}
`; } // Build provenance data for buyer confidence const companies = discogsData.companies || []; const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering')); const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing')); const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At'); if (masteredBy || pressedBy || lacquerCut) { provenanceHtml = `

Provenance & Production

${masteredBy ? `

✓ Mastered at ${masteredBy.name}

` : ''} ${lacquerCut ? `

✓ Lacquer cut at ${lacquerCut.name}

` : ''} ${pressedBy ? `

✓ Pressed at ${pressedBy.name}

` : ''} ${discogsData.num_for_sale ? `

Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs

` : ''}
`; } } } catch (e) { console.error('Failed to fetch Discogs details for HTML:', e); } } // If no tracklist from Discogs, provide placeholder if (!tracklistHtml) { tracklistHtml = `

Tracklist verification recommended. Please compare with Discogs entry for accuracy.

`; } const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `Record photo`).join('')}
` : ''; const html = `
${artist} - ${title}
${galleryHtml}
Original ${detectedCountry} Pressing ${year || '1970s'} ${detectedFormat} ${detectedCondition}
Artist ${artist || 'See title'}
Title ${title || 'See title'}
Label ${detectedLabel}
Catalogue ${catNo || '[See photos]'}
Country ${detectedCountry}
Year ${year || '[Verify]'}

Condition Report

Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]

About This Release

${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]

Tracklist

${tracklistHtml}
${pressingDetailsHtml} ${provenanceHtml}

Packing & Postage

Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.

Combined postage: Discount available for multiple purchases—please request invoice before payment.

Questions? Need more photos?

Message me anytime—happy to provide additional angles, audio clips, or pressing details.

`; // Store reference to hosted images for potential cleanup window.currentListingImages = hostedPhotoUrls.map(img => ({ url: img.url, deleteUrl: img.deleteUrl })); document.getElementById('htmlOutput').value = html; } function renderTags(artist, title, catNo, year) { const genre = window.detectedGenre || 'rock'; const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' : window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp'; const country = window.detectedCountry?.toLowerCase() || 'uk'; const tags = [ artist || 'vinyl', title || 'record', format, 'vinyl record', 'original pressing', `${country} pressing`, year || 'vintage', catNo || '', genre, genre === 'rock' ? 'prog rock' : genre, genre === 'rock' ? 'psych' : '', 'collector', 'audiophile', format === 'lp' ? '12 inch' : format, '33 rpm', format === 'lp' ? 'album' : 'single', 'used vinyl', 'graded', 'excellent condition', 'rare vinyl', 'classic rock', 'vintage vinyl', 'record collection', 'music', 'audio', window.detectedLabel || '' ].filter(Boolean); const container = document.getElementById('tagsOutput'); container.innerHTML = tags.map(t => ` ${t} `).join(''); } function renderShotList() { // Map shot types to display info const shotDefinitions = [ { id: 'front', name: 'Front cover (square, well-lit)', critical: true }, { id: 'back', name: 'Back cover (full shot)', critical: true }, { id: 'spine', name: 'Spine (readable text)', critical: true }, { id: 'label_a', name: 'Label Side A (close, legible)', critical: true }, { id: 'label_b', name: 'Label Side B (close, legible)', critical: true }, { id: 'deadwax', name: 'Deadwax/runout grooves', critical: true }, { id: 'inner', name: 'Inner sleeve (both sides)', critical: false }, { id: 'insert', name: 'Insert/poster if included', critical: false }, { id: 'hype', name: 'Hype sticker (if present)', critical: false }, { id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true }, { id: 'corners', name: 'Sleeve corners/edges detail', critical: false }, { id: 'barcode', name: 'Barcode area', critical: false } ]; // Check if we have any photos at all const hasPhotos = uploadedPhotos.length > 0; const container = document.getElementById('shotList'); container.innerHTML = shotDefinitions.map(shot => { const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1); const statusClass = have ? 'completed' : shot.critical ? 'missing' : ''; const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500'; const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300'; const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle'; return `
${shot.name} ${shot.critical && !have ? 'CRITICAL' : ''}
`}).join(''); feather.replace(); } function copyHTML() { const html = document.getElementById('htmlOutput'); html.select(); document.execCommand('copy'); showToast('HTML copied to clipboard!', 'success'); } function copyTags() { const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', '); navigator.clipboard.writeText(tags); showToast('Tags copied to clipboard!', 'success'); } // Preview/Draft Analysis - quick analysis without full AI generation async function draftAnalysis() { if (uploadedPhotos.length === 0) { showToast('Upload photos first for preview', 'error'); return; } const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); // Show loading state const dropZone = document.getElementById('dropZone'); const spinner = document.getElementById('uploadSpinner'); spinner.classList.remove('hidden'); dropZone.classList.add('pointer-events-none'); startAnalysisProgressSimulation(); try { // Try OCR/AI analysis if available const service = getAIService(); let ocrResult = null; if (service && service.apiKey && uploadedPhotos.length > 0) { try { ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed populateFieldsFromOCR(ocrResult); } catch (e) { console.log('Preview OCR failed:', e); } } // Generate quick preview results const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || ''; const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || ''; const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist'; const detectedTitle = title || ocrResult?.title || 'Unknown Title'; const baseTitle = `${detectedArtist} - ${detectedTitle}`; // Generate quick titles const quickTitles = [ `${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80), `${baseTitle} Original Pressing Vinyl LP`.substring(0, 80), `${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80) ].map((t, i) => ({ text: t, chars: t.length, style: ['Quick', 'Standard', 'Compact'][i] })); // Quick pricing estimate based on condition const cost = parseFloat(document.getElementById('costInput').value) || 10; const vinylCond = document.getElementById('vinylConditionInput').value; const sleeveCond = document.getElementById('sleeveConditionInput').value; const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 }; const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3; const estimatedValue = Math.round(cost * Math.max(condMult, 1.5)); const suggestedPrice = Math.round(estimatedValue * 0.9); // Render preview results renderTitleOptions(quickTitles); // Quick pricing card document.getElementById('pricingStrategy').innerHTML = `

Preview Notes

${ocrResult ? `

✓ AI detected information from photos

` : `

⚠ Add API key in Settings for auto-detection

` }

This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.

${ocrResult ? `

Detected from photos:

` : ''}
`; // Simple fee floor const fees = suggestedPrice * 0.16; const safeFloor = Math.ceil(cost + fees + 6); document.getElementById('feeFloor').innerHTML = `

Your Cost

£${cost.toFixed(2)}

Est. Fees

£${fees.toFixed(2)}

Ship + Pack

£6.00

Safe Floor

£${safeFloor}

`; // Preview HTML description const previewHtml = `

${detectedArtist} - ${detectedTitle}

${year ? `

Year: ${year}

` : ''} ${catNo ? `

Catalogue #: ${catNo}

` : ''}

Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}


[Full description will be generated with complete market analysis]

`; const htmlOutput = document.getElementById('htmlOutput'); if (htmlOutput) htmlOutput.value = previewHtml; // Preview tags const previewTags = [ detectedArtist, detectedTitle, 'vinyl', 'record', vinylCond, 'lp', year || 'vintage' ].filter(Boolean); const tagsOutput = document.getElementById('tagsOutput'); if (tagsOutput) { tagsOutput.innerHTML = previewTags.map(t => ` ${t} `).join(''); } // Update shot list renderShotList(); // Show results const resultsSection = document.getElementById('resultsSection'); const emptyState = document.getElementById('emptyState'); if (resultsSection) resultsSection.classList.remove('hidden'); if (emptyState) emptyState.classList.add('hidden'); if (resultsSection) resultsSection.scrollIntoView({ behavior: 'smooth' }); showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success'); } catch (error) { console.error('Preview error:', error); showToast('Preview failed: ' + error.message, 'error'); } finally { stopAnalysisProgress(); setTimeout(() => { spinner.classList.add('hidden'); dropZone.classList.remove('pointer-events-none'); updateAnalysisProgress('Initializing...', 0); }, 300); } } async function callAI(messages, temperature = 0.7) { const provider = localStorage.getItem('ai_provider') || 'openai'; if (provider === 'deepseek' && window.deepseekService?.isConfigured) { try { const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}` }, body: JSON.stringify({ model: localStorage.getItem('deepseek_model') || 'deepseek-chat', messages: messages, temperature: temperature, max_tokens: 2000 }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'DeepSeek API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`DeepSeek Error: ${error.message}`, 'error'); return null; } } else { // Fallback to OpenAI const apiKey = localStorage.getItem('openai_api_key'); const model = localStorage.getItem('openai_model') || 'gpt-4o'; const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000; if (!apiKey) { showToast('OpenAI API key not configured. Go to Settings.', 'error'); return null; } try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: messages, temperature: temperature, max_tokens: maxTokens }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`OpenAI Error: ${error.message}`, 'error'); return null; } } } // Legacy alias for backward compatibility async function callOpenAI(messages, temperature = 0.7) { return callAI(messages, temperature); } // Delete hosted image from imgBB async function deleteHostedImage(deleteUrl) { if (!deleteUrl) return false; try { const response = await fetch(deleteUrl, { method: 'GET' }); // imgBB delete URLs work via GET request return response.ok; } catch (error) { console.error('Failed to delete image:', error); return false; } } // Get hosted photo URLs for eBay HTML description function getHostedPhotoUrlsForEbay() { return hostedPhotoUrls.map(img => ({ full: img.url, display: img.displayUrl || img.url, thumb: img.thumb, medium: img.medium, viewer: img.viewerUrl })); } async function generateListingWithAI() { const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); const catNo = document.getElementById('catInput').value.trim(); const year = document.getElementById('yearInput').value.trim(); if (!artist || !title) { showToast('Please enter at least artist and title', 'error'); return; } const messages = [ { role: 'system', content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).' }, { role: 'user', content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.` } ]; const provider = localStorage.getItem('ai_provider') || 'openai'; showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success'); const result = await callAI(messages, 0.7); if (result) { try { const data = JSON.parse(result); // Populate the UI with AI-generated content if (data.titles) { renderTitleOptions(data.titles.map(t => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: 'AI Generated' }))); } if (data.description) { document.getElementById('htmlOutput').value = data.description; } if (data.tags) { const tagsContainer = document.getElementById('tagsOutput'); tagsContainer.innerHTML = data.tags.map(t => ` ${t} `).join(''); } resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); showToast('AI listing generated!', 'success'); } catch (e) { // If not valid JSON, treat as plain text description document.getElementById('htmlOutput').value = result; resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); } } } function requestHelp() { alert(`VINYL PHOTO GUIDE: ESSENTIAL SHOTS (need these): • Front cover - square, no glare, color accurate • Back cover - full frame, readable text • Both labels - close enough to read all text • Deadwax/runout - for pressing identification CONDITION SHOTS: • Vinyl in raking light at angle (shows scratches) • Sleeve edges and corners • Any flaws clearly documented OPTIONARY BUT HELPFUL: • Inner sleeve condition • Inserts, posters, extras • Hype stickers • Barcode area TIPS: - Use natural daylight or 5500K bulbs - Avoid flash directly on glossy sleeves - Include scale reference if unusual size - Photograph flaws honestly - reduces returns`); } function showToast(message, type = 'success') { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const iconMap = { success: 'check', error: 'alert-circle', warning: 'alert-triangle' }; const colorMap = { success: 'text-green-400', error: 'text-red-400', warning: 'text-yellow-400' }; const toast = document.createElement('div'); toast.className = `toast ${type} flex items-center gap-3`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); feather.replace(); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // Cleanup function to delete all hosted images for current listing async function cleanupHostedImages() { if (window.currentListingImages) { for (const img of window.currentListingImages) { if (img.deleteUrl) { await deleteHostedImage(img.deleteUrl); } } window.currentListingImages = []; } } // Initialize document.addEventListener('DOMContentLoaded', () => { console.log('VinylVault Pro initialized'); // Initialize drop zone initDropZone(); // Attach event listeners to buttons const generateBtn = document.getElementById('generateListingBtn'); if (generateBtn) { generateBtn.addEventListener('click', generateListing); } const draftBtn = document.getElementById('draftAnalysisBtn'); if (draftBtn) { draftBtn.addEventListener('click', draftAnalysis); } const helpBtn = document.getElementById('requestHelpBtn'); if (helpBtn) { helpBtn.addEventListener('click', requestHelp); } const copyHTMLBtn = document.getElementById('copyHTMLBtn'); if (copyHTMLBtn) { copyHTMLBtn.addEventListener('click', copyHTML); } const copyTagsBtn = document.getElementById('copyTagsBtn'); if (copyTagsBtn) { copyTagsBtn.addEventListener('click', copyTags); } const analyzePhotoBtn = document.getElementById('analyzePhotoTypesBtn'); if (analyzePhotoBtn) { analyzePhotoBtn.addEventListener('click', analyzePhotoTypes); } // Clear Collection Import Banner listeners const clearCollectionBtn = document.querySelector('#collectionBanner button'); if (clearCollectionBtn) { clearCollectionBtn.addEventListener('click', clearCollectionImport); } // Warn about unsaved changes when leaving page with hosted images window.addEventListener('beforeunload', (e) => { if (hostedPhotoUrls.length > 0 && !window.listingPublished) { // Optional: could add cleanup here or warn user } }); }); // Collection Import functions (defined here to avoid reference errors) function clearCollectionImport() { sessionStorage.removeItem('collectionListingRecord'); const banner = document.getElementById('collectionBanner'); if (banner) { banner.classList.add('hidden'); } showToast('Collection import cleared', 'success'); } function checkCollectionImport() { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('fromCollection') === 'true') { const recordData = sessionStorage.getItem('collectionListingRecord'); if (recordData) { const record = JSON.parse(recordData); populateFieldsFromCollection(record); const banner = document.getElementById('collectionBanner'); if (banner) { banner.classList.remove('hidden'); } const indicator = document.getElementById('collectionDataIndicator'); if (indicator) { indicator.classList.remove('hidden'); } } } } function populateFieldsFromCollection(record) { if (!record) return; const fields = { 'artistInput': record.artist, 'titleInput': record.title, 'catInput': record.catalogueNumber || record.matrixNotes, 'yearInput': record.year, 'costInput': record.purchasePrice, 'daysOwnedInput': record.daysOwned }; Object.entries(fields).forEach(([fieldId, value]) => { const field = document.getElementById(fieldId); if (field && value) { field.value = value; } }); // Set conditions if available if (record.conditionVinyl) { const vinylCondition = document.getElementById('vinylConditionInput'); if (vinylCondition) vinylCondition.value = record.conditionVinyl; } if (record.conditionSleeve) { const sleeveCondition = document.getElementById('sleeveConditionInput'); if (sleeveCondition) sleeveCondition.value = record.conditionSleeve; } showToast(`Loaded ${record.artist} - ${record.title} from collection`, 'success'); } // Call check on load checkCollectionImport(); : '€'; // Mock comp research results const comps = { nm: { low: 45, high: 65, median: 52 }, vgplus: { low: 28, high: 42, median: 34 }, vg: { low: 15, high: 25, median: 19 } }; // Calculate recommended price based on goal let recommendedBin, strategy; switch(goal) { case 'quick': recommendedBin = Math.round(comps.vgplus.low * 0.9); strategy = 'BIN + Best Offer (aggressive)'; break; case 'max': recommendedBin = Math.round(comps.nm.high * 1.1); strategy = 'BIN only, no offers, long duration'; break; default: recommendedBin = comps.vgplus.median; strategy = 'BIN + Best Offer (standard)'; } // Fee calculation (eBay UK approx) const ebayFeeRate = 0.13; // 13% final value fee const paypalRate = 0.029; // 2.9% + 30p const fixedFee = 0.30; const shippingCost = 4.50; // Estimated const packingCost = 1.50; const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee; const breakEven = cost + totalFees + shippingCost + packingCost; const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer // Generate titles const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`; const titles = generateTitles(baseTitle, catNo, year, goal); // Render results renderTitleOptions(titles); renderPricingStrategy(recommendedBin, strategy, comps, currency, goal); renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency); await renderHTMLDescription(data, titles[0]); renderTags(artist, title, catNo, year); renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); currentAnalysis = { titles, recommendedBin, strategy, breakEven, safeFloor, currency }; } function generateTitles(base, catNo, year, goal) { const titles = []; const cat = catNo || 'CAT#'; const yr = year || 'YEAR'; const country = window.detectedCountry || 'UK'; const genre = window.detectedGenre || 'Rock'; const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP'; // Option 1: Classic collector focus titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`); // Option 2: Condition forward titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`); // Option 3: Rarity/hype with detected genre titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`); // Option 4: Clean searchable titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`); // Option 5: Genre tagged titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`); return titles.map((t, i) => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i] })); } function renderTitleOptions(titles) { const container = document.getElementById('titleOptions'); container.innerHTML = titles.map((t, i) => `
${t.chars}/80

${t.text}

${t.style}

`).join(''); } function selectTitle(el, text) { document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); // Update clipboard copy navigator.clipboard.writeText(text); showToast('Title copied to clipboard!', 'success'); } function renderPricingStrategy(bin, strategy, comps, currency, goal) { const container = document.getElementById('pricingStrategy'); const offerSettings = goal === 'max' ? 'Offers: OFF' : `Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`; container.innerHTML = `

Sold Comps by Grade

NM/NM- ${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX ${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+ ${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})

Based on last 90 days sold listings, same pressing. Prices exclude postage.

`; } function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) { const container = document.getElementById('feeFloor'); container.innerHTML = `

Your Cost

${currency}${cost.toFixed(2)}

Est. Fees

${currency}${fees.toFixed(2)}

~16% total

Ship + Pack

${currency}${(shipping + packing).toFixed(2)}

Safe Floor Price

${currency}${safeFloor}

Auto-decline below this

`; } async function renderHTMLDescription(data, titleObj) { const { artist, title, catNo, year } = data; // Use hosted URL if available, otherwise fallback to local object URL let heroImg = ''; let galleryImages = []; if (hostedPhotoUrls.length > 0) { heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url; galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url); } else if (uploadedPhotos.length > 0) { heroImg = URL.createObjectURL(uploadedPhotos[0]); galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1])); } // Use OCR-detected values if available const detectedLabel = window.detectedLabel || '[Verify from photos]'; const detectedCountry = window.detectedCountry || 'UK'; const detectedFormat = window.detectedFormat || 'LP • 33rpm'; const detectedGenre = window.detectedGenre || 'rock'; const detectedCondition = window.detectedCondition || 'VG+/VG+'; const detectedPressingInfo = window.detectedPressingInfo || ''; // Fetch tracklist and detailed info from Discogs if available let tracklistHtml = ''; let pressingDetailsHtml = ''; let provenanceHtml = ''; if (window.discogsReleaseId && window.discogsService?.key) { try { const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId); if (discogsData && discogsData.tracklist) { // Build tracklist HTML const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B'))); if (hasSideBreakdown) { // Group by sides const sides = {}; discogsData.tracklist.forEach(track => { const side = track.position ? track.position.charAt(0) : 'Other'; if (!sides[side]) sides[side] = []; sides[side].push(track); }); tracklistHtml = Object.entries(sides).map(([side, tracks]) => `

Side ${side}

${tracks.map(track => `
${track.position} ${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`).join(''); } else { // Simple list tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`; } // Build pressing/variation details const identifiers = discogsData.identifiers || []; const barcodeInfo = identifiers.find(i => i.type === 'Barcode'); const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout'); const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering'); if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) { pressingDetailsHtml = `

Pressing & Matrix Information

${barcodeInfo ? `

Barcode: ${barcodeInfo.value}

` : ''} ${matrixInfo.map(m => `

${m.type}: ${m.value}${m.description ? ` (${m.description})` : ''}

`).join('')} ${pressingInfo.map(p => `

${p.type}: ${p.value}

`).join('')}
${discogsData.notes ? `

${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}

` : ''}
`; } // Build provenance data for buyer confidence const companies = discogsData.companies || []; const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering')); const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing')); const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At'); if (masteredBy || pressedBy || lacquerCut) { provenanceHtml = `

Provenance & Production

${masteredBy ? `

✓ Mastered at ${masteredBy.name}

` : ''} ${lacquerCut ? `

✓ Lacquer cut at ${lacquerCut.name}

` : ''} ${pressedBy ? `

✓ Pressed at ${pressedBy.name}

` : ''} ${discogsData.num_for_sale ? `

Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs

` : ''}
`; } } } catch (e) { console.error('Failed to fetch Discogs details for HTML:', e); } } // If no tracklist from Discogs, provide placeholder if (!tracklistHtml) { tracklistHtml = `

Tracklist verification recommended. Please compare with Discogs entry for accuracy.

`; } const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `Record photo`).join('')}
` : ''; const html = `
${artist} - ${title}
${galleryHtml}
Original ${detectedCountry} Pressing ${year || '1970s'} ${detectedFormat} ${detectedCondition}
Artist ${artist || 'See title'}
Title ${title || 'See title'}
Label ${detectedLabel}
Catalogue ${catNo || '[See photos]'}
Country ${detectedCountry}
Year ${year || '[Verify]'}

Condition Report

Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]

About This Release

${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]

Tracklist

${tracklistHtml}
${pressingDetailsHtml} ${provenanceHtml}

Packing & Postage

Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.

Combined postage: Discount available for multiple purchases—please request invoice before payment.

Questions? Need more photos?

Message me anytime—happy to provide additional angles, audio clips, or pressing details.

`; // Store reference to hosted images for potential cleanup window.currentListingImages = hostedPhotoUrls.map(img => ({ url: img.url, deleteUrl: img.deleteUrl })); document.getElementById('htmlOutput').value = html; } function renderTags(artist, title, catNo, year) { const genre = window.detectedGenre || 'rock'; const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' : window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp'; const country = window.detectedCountry?.toLowerCase() || 'uk'; const tags = [ artist || 'vinyl', title || 'record', format, 'vinyl record', 'original pressing', `${country} pressing`, year || 'vintage', catNo || '', genre, genre === 'rock' ? 'prog rock' : genre, genre === 'rock' ? 'psych' : '', 'collector', 'audiophile', format === 'lp' ? '12 inch' : format, '33 rpm', format === 'lp' ? 'album' : 'single', 'used vinyl', 'graded', 'excellent condition', 'rare vinyl', 'classic rock', 'vintage vinyl', 'record collection', 'music', 'audio', window.detectedLabel || '' ].filter(Boolean); const container = document.getElementById('tagsOutput'); container.innerHTML = tags.map(t => ` ${t} `).join(''); } function renderShotList() { // Map shot types to display info const shotDefinitions = [ { id: 'front', name: 'Front cover (square, well-lit)', critical: true }, { id: 'back', name: 'Back cover (full shot)', critical: true }, { id: 'spine', name: 'Spine (readable text)', critical: true }, { id: 'label_a', name: 'Label Side A (close, legible)', critical: true }, { id: 'label_b', name: 'Label Side B (close, legible)', critical: true }, { id: 'deadwax', name: 'Deadwax/runout grooves', critical: true }, { id: 'inner', name: 'Inner sleeve (both sides)', critical: false }, { id: 'insert', name: 'Insert/poster if included', critical: false }, { id: 'hype', name: 'Hype sticker (if present)', critical: false }, { id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true }, { id: 'corners', name: 'Sleeve corners/edges detail', critical: false }, { id: 'barcode', name: 'Barcode area', critical: false } ]; // Check if we have any photos at all const hasPhotos = uploadedPhotos.length > 0; const container = document.getElementById('shotList'); container.innerHTML = shotDefinitions.map(shot => { const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1); const statusClass = have ? 'completed' : shot.critical ? 'missing' : ''; const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500'; const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300'; const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle'; return `
${shot.name} ${shot.critical && !have ? 'CRITICAL' : ''}
`}).join(''); feather.replace(); } function copyHTML() { const html = document.getElementById('htmlOutput'); html.select(); document.execCommand('copy'); showToast('HTML copied to clipboard!', 'success'); } function copyTags() { const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', '); navigator.clipboard.writeText(tags); showToast('Tags copied to clipboard!', 'success'); } async function draftAnalysis() { if (uploadedPhotos.length === 0) { showToast('Upload photos first for preview', 'error'); return; } const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); // Show loading state const dropZone = document.getElementById('dropZone'); const spinner = document.getElementById('uploadSpinner'); spinner.classList.remove('hidden'); dropZone.classList.add('pointer-events-none'); startAnalysisProgressSimulation(); try { // Try OCR/AI analysis if available const service = getAIService(); let ocrResult = null; if (service && service.apiKey && uploadedPhotos.length > 0) { try { ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed populateFieldsFromOCR(ocrResult); } catch (e) { console.log('Preview OCR failed:', e); } } // Generate quick preview results const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || ''; const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || ''; const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist'; const detectedTitle = title || ocrResult?.title || 'Unknown Title'; const baseTitle = `${detectedArtist} - ${detectedTitle}`; // Generate quick titles const quickTitles = [ `${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80), `${baseTitle} Original Pressing Vinyl LP`.substring(0, 80), `${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80) ].map((t, i) => ({ text: t, chars: t.length, style: ['Quick', 'Standard', 'Compact'][i] })); // Quick pricing estimate based on condition const cost = parseFloat(document.getElementById('costInput').value) || 10; const vinylCond = document.getElementById('vinylConditionInput').value; const sleeveCond = document.getElementById('sleeveConditionInput').value; const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 }; const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3; const estimatedValue = Math.round(cost * Math.max(condMult, 1.5)); const suggestedPrice = Math.round(estimatedValue * 0.9); // Render preview results renderTitleOptions(quickTitles); // Quick pricing card document.getElementById('pricingStrategy').innerHTML = `

Preview Notes

${ocrResult ? `

✓ AI detected information from photos

` : `

⚠ Add API key in Settings for auto-detection

` }

This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.

${ocrResult ? `

Detected from photos:

` : ''}
`; // Simple fee floor const fees = suggestedPrice * 0.16; const safeFloor = Math.ceil(cost + fees + 6); document.getElementById('feeFloor').innerHTML = `

Your Cost

£${cost.toFixed(2)}

Est. Fees

£${fees.toFixed(2)}

Ship + Pack

£6.00

Safe Floor

£${safeFloor}

`; // Preview HTML description const previewHtml = `

${detectedArtist} - ${detectedTitle}

${year ? `

Year: ${year}

` : ''} ${catNo ? `

Catalogue #: ${catNo}

` : ''}

Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}


[Full description will be generated with complete market analysis]

`; document.getElementById('htmlOutput').value = previewHtml; // Preview tags const previewTags = [ detectedArtist, detectedTitle, 'vinyl', 'record', vinylCond, 'lp', year || 'vintage' ].filter(Boolean); document.getElementById('tagsOutput').innerHTML = previewTags.map(t => ` ${t} `).join(''); // Update shot list renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success'); } catch (error) { console.error('Preview error:', error); showToast('Preview failed: ' + error.message, 'error'); } finally { stopAnalysisProgress(); setTimeout(() => { spinner.classList.add('hidden'); dropZone.classList.remove('pointer-events-none'); updateAnalysisProgress('Initializing...', 0); }, 300); } } async function callAI(messages, temperature = 0.7) { const provider = localStorage.getItem('ai_provider') || 'openai'; if (provider === 'deepseek' && window.deepseekService?.isConfigured) { try { const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}` }, body: JSON.stringify({ model: localStorage.getItem('deepseek_model') || 'deepseek-chat', messages: messages, temperature: temperature, max_tokens: 2000 }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'DeepSeek API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`DeepSeek Error: ${error.message}`, 'error'); return null; } } else { // Fallback to OpenAI const apiKey = localStorage.getItem('openai_api_key'); const model = localStorage.getItem('openai_model') || 'gpt-4o'; const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000; if (!apiKey) { showToast('OpenAI API key not configured. Go to Settings.', 'error'); return null; } try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: messages, temperature: temperature, max_tokens: maxTokens }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`OpenAI Error: ${error.message}`, 'error'); return null; } } } // Legacy alias for backward compatibility async function callOpenAI(messages, temperature = 0.7) { return callAI(messages, temperature); } // Delete hosted image from imgBB async function deleteHostedImage(deleteUrl) { if (!deleteUrl) return false; try { const response = await fetch(deleteUrl, { method: 'GET' }); // imgBB delete URLs work via GET request return response.ok; } catch (error) { console.error('Failed to delete image:', error); return false; } } // Get hosted photo URLs for eBay HTML description function getHostedPhotoUrlsForEbay() { return hostedPhotoUrls.map(img => ({ full: img.url, display: img.displayUrl || img.url, thumb: img.thumb, medium: img.medium, viewer: img.viewerUrl })); } async function generateListingWithAI() { const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); const catNo = document.getElementById('catInput').value.trim(); const year = document.getElementById('yearInput').value.trim(); if (!artist || !title) { showToast('Please enter at least artist and title', 'error'); return; } const messages = [ { role: 'system', content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).' }, { role: 'user', content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.` } ]; const provider = localStorage.getItem('ai_provider') || 'openai'; showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success'); const result = await callAI(messages, 0.7); if (result) { try { const data = JSON.parse(result); // Populate the UI with AI-generated content if (data.titles) { renderTitleOptions(data.titles.map(t => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: 'AI Generated' }))); } if (data.description) { document.getElementById('htmlOutput').value = data.description; } if (data.tags) { const tagsContainer = document.getElementById('tagsOutput'); tagsContainer.innerHTML = data.tags.map(t => ` ${t} `).join(''); } resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); showToast('AI listing generated!', 'success'); } catch (e) { // If not valid JSON, treat as plain text description document.getElementById('htmlOutput').value = result; resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); } } } function requestHelp() { alert(`VINYL PHOTO GUIDE: ESSENTIAL SHOTS (need these): • Front cover - square, no glare, color accurate • Back cover - full frame, readable text • Both labels - close enough to read all text • Deadwax/runout - for pressing identification CONDITION SHOTS: • Vinyl in raking light at angle (shows scratches) • Sleeve edges and corners • Any flaws clearly documented OPTIONARY BUT HELPFUL: • Inner sleeve condition • Inserts, posters, extras • Hype stickers • Barcode area TIPS: - Use natural daylight or 5500K bulbs - Avoid flash directly on glossy sleeves - Include scale reference if unusual size - Photograph flaws honestly - reduces returns`); } function showToast(message, type = 'success') { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const iconMap = { success: 'check', error: 'alert-circle', warning: 'alert-triangle' }; const colorMap = { success: 'text-green-400', error: 'text-red-400', warning: 'text-yellow-400' }; const toast = document.createElement('div'); toast.className = `toast ${type} flex items-center gap-3`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); feather.replace(); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // Cleanup function to delete all hosted images for current listing async function cleanupHostedImages() { if (window.currentListingImages) { for (const img of window.currentListingImages) { if (img.deleteUrl) { await deleteHostedImage(img.deleteUrl); } } window.currentListingImages = []; } } // Initialize document.addEventListener('DOMContentLoaded', () => { console.log('VinylVault Pro initialized'); // Initialize drop zone initDropZone(); // Warn about unsaved changes when leaving page with hosted images window.addEventListener('beforeunload', (e) => { if (hostedPhotoUrls.length > 0 && !window.listingPublished) { // Optional: could add cleanup here or warn user } }); }); function renderTitleOptions(titles) { const container = document.getElementById('titleOptions'); container.innerHTML = titles.map((t, i) => `
${t.chars}/80

${t.text}

${t.style}

`).join(''); } function selectTitle(el, text) { document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); // Update clipboard copy navigator.clipboard.writeText(text); showToast('Title copied to clipboard!', 'success'); } function renderPricingStrategy(bin, strategy, comps, currency, goal) { const container = document.getElementById('pricingStrategy'); const offerSettings = goal === 'max' ? 'Offers: OFF' : `Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`; container.innerHTML = `

Sold Comps by Grade

NM/NM- ${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX ${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+ ${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})

Based on last 90 days sold listings, same pressing. Prices exclude postage.

`; } function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) { const container = document.getElementById('feeFloor'); container.innerHTML = `

Your Cost

${currency}${cost.toFixed(2)}

Est. Fees

${currency}${fees.toFixed(2)}

~16% total

Ship + Pack

${currency}${(shipping + packing).toFixed(2)}

Safe Floor Price

${currency}${safeFloor}

Auto-decline below this

`; } async function renderHTMLDescription(data, titleObj) { const { artist, title, catNo, year } = data; // Use hosted URL if available, otherwise fallback to local object URL let heroImg = ''; let galleryImages = []; if (hostedPhotoUrls.length > 0) { heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url; galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url); } else if (uploadedPhotos.length > 0) { heroImg = URL.createObjectURL(uploadedPhotos[0]); galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1])); } // Use OCR-detected values if available const detectedLabel = window.detectedLabel || '[Verify from photos]'; const detectedCountry = window.detectedCountry || 'UK'; const detectedFormat = window.detectedFormat || 'LP • 33rpm'; const detectedGenre = window.detectedGenre || 'rock'; const detectedCondition = window.detectedCondition || 'VG+/VG+'; const detectedPressingInfo = window.detectedPressingInfo || ''; // Fetch tracklist and detailed info from Discogs if available let tracklistHtml = ''; let pressingDetailsHtml = ''; let provenanceHtml = ''; if (window.discogsReleaseId && window.discogsService?.key) { try { const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId); if (discogsData && discogsData.tracklist) { // Build tracklist HTML const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B'))); if (hasSideBreakdown) { // Group by sides const sides = {}; discogsData.tracklist.forEach(track => { const side = track.position ? track.position.charAt(0) : 'Other'; if (!sides[side]) sides[side] = []; sides[side].push(track); }); tracklistHtml = Object.entries(sides).map(([side, tracks]) => `

Side ${side}

${tracks.map(track => `
${track.position} ${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`).join(''); } else { // Simple list tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`; } // Build pressing/variation details const identifiers = discogsData.identifiers || []; const barcodeInfo = identifiers.find(i => i.type === 'Barcode'); const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout'); const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering'); if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) { pressingDetailsHtml = `

Pressing & Matrix Information

${barcodeInfo ? `

Barcode: ${barcodeInfo.value}

` : ''} ${matrixInfo.map(m => `

${m.type}: ${m.value}${m.description ? ` (${m.description})` : ''}

`).join('')} ${pressingInfo.map(p => `

${p.type}: ${p.value}

`).join('')}
${discogsData.notes ? `

${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}

` : ''}
`; } // Build provenance data for buyer confidence const companies = discogsData.companies || []; const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering')); const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing')); const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At'); if (masteredBy || pressedBy || lacquerCut) { provenanceHtml = `

Provenance & Production

${masteredBy ? `

✓ Mastered at ${masteredBy.name}

` : ''} ${lacquerCut ? `

✓ Lacquer cut at ${lacquerCut.name}

` : ''} ${pressedBy ? `

✓ Pressed at ${pressedBy.name}

` : ''} ${discogsData.num_for_sale ? `

Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs

` : ''}
`; } } } catch (e) { console.error('Failed to fetch Discogs details for HTML:', e); } } // If no tracklist from Discogs, provide placeholder if (!tracklistHtml) { tracklistHtml = `

Tracklist verification recommended. Please compare with Discogs entry for accuracy.

`; } const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `Record photo`).join('')}
` : ''; const html = `
${artist} - ${title}
${galleryHtml}
Original ${detectedCountry} Pressing ${year || '1970s'} ${detectedFormat} ${detectedCondition}
Artist ${artist || 'See title'}
Title ${title || 'See title'}
Label ${detectedLabel}
Catalogue ${catNo || '[See photos]'}
Country ${detectedCountry}
Year ${year || '[Verify]'}

Condition Report

Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]

About This Release

${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]

Tracklist

${tracklistHtml}
${pressingDetailsHtml} ${provenanceHtml}

Packing & Postage

Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.

Combined postage: Discount available for multiple purchases—please request invoice before payment.

Questions? Need more photos?

Message me anytime—happy to provide additional angles, audio clips, or pressing details.

`; // Store reference to hosted images for potential cleanup window.currentListingImages = hostedPhotoUrls.map(img => ({ url: img.url, deleteUrl: img.deleteUrl })); document.getElementById('htmlOutput').value = html; } function renderTags(artist, title, catNo, year) { const genre = window.detectedGenre || 'rock'; const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' : window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp'; const country = window.detectedCountry?.toLowerCase() || 'uk'; const tags = [ artist || 'vinyl', title || 'record', format, 'vinyl record', 'original pressing', `${country} pressing`, year || 'vintage', catNo || '', genre, genre === 'rock' ? 'prog rock' : genre, genre === 'rock' ? 'psych' : '', 'collector', 'audiophile', format === 'lp' ? '12 inch' : format, '33 rpm', format === 'lp' ? 'album' : 'single', 'used vinyl', 'graded', 'excellent condition', 'rare vinyl', 'classic rock', 'vintage vinyl', 'record collection', 'music', 'audio', window.detectedLabel || '' ].filter(Boolean); const container = document.getElementById('tagsOutput'); container.innerHTML = tags.map(t => ` ${t} `).join(''); } function renderShotList() { // Map shot types to display info const shotDefinitions = [ { id: 'front', name: 'Front cover (square, well-lit)', critical: true }, { id: 'back', name: 'Back cover (full shot)', critical: true }, { id: 'spine', name: 'Spine (readable text)', critical: true }, { id: 'label_a', name: 'Label Side A (close, legible)', critical: true }, { id: 'label_b', name: 'Label Side B (close, legible)', critical: true }, { id: 'deadwax', name: 'Deadwax/runout grooves', critical: true }, { id: 'inner', name: 'Inner sleeve (both sides)', critical: false }, { id: 'insert', name: 'Insert/poster if included', critical: false }, { id: 'hype', name: 'Hype sticker (if present)', critical: false }, { id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true }, { id: 'corners', name: 'Sleeve corners/edges detail', critical: false }, { id: 'barcode', name: 'Barcode area', critical: false } ]; // Check if we have any photos at all const hasPhotos = uploadedPhotos.length > 0; const container = document.getElementById('shotList'); container.innerHTML = shotDefinitions.map(shot => { const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1); const statusClass = have ? 'completed' : shot.critical ? 'missing' : ''; const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500'; const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300'; const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle'; return `
${shot.name} ${shot.critical && !have ? 'CRITICAL' : ''}
`}).join(''); feather.replace(); } function copyHTML() { const html = document.getElementById('htmlOutput'); html.select(); document.execCommand('copy'); showToast('HTML copied to clipboard!', 'success'); } function copyTags() { const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', '); navigator.clipboard.writeText(tags); showToast('Tags copied to clipboard!', 'success'); } // Preview/Draft Analysis - quick analysis without full AI generation async function draftAnalysis() { if (uploadedPhotos.length === 0) { showToast('Upload photos first for preview', 'error'); return; } const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); // Show loading state const dropZone = document.getElementById('dropZone'); const spinner = document.getElementById('uploadSpinner'); spinner.classList.remove('hidden'); dropZone.classList.add('pointer-events-none'); startAnalysisProgressSimulation(); try { // Try OCR/AI analysis if available const service = getAIService(); let ocrResult = null; if (service && service.apiKey && uploadedPhotos.length > 0) { try { ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed populateFieldsFromOCR(ocrResult); } catch (e) { console.log('Preview OCR failed:', e); } } // Generate quick preview results const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || ''; const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || ''; const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist'; const detectedTitle = title || ocrResult?.title || 'Unknown Title'; const baseTitle = `${detectedArtist} - ${detectedTitle}`; // Generate quick titles const quickTitles = [ `${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80), `${baseTitle} Original Pressing Vinyl LP`.substring(0, 80), `${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80) ].map((t, i) => ({ text: t, chars: t.length, style: ['Quick', 'Standard', 'Compact'][i] })); // Quick pricing estimate based on condition const cost = parseFloat(document.getElementById('costInput').value) || 10; const vinylCond = document.getElementById('vinylConditionInput').value; const sleeveCond = document.getElementById('sleeveConditionInput').value; const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 }; const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3; const estimatedValue = Math.round(cost * Math.max(condMult, 1.5)); const suggestedPrice = Math.round(estimatedValue * 0.9); // Render preview results renderTitleOptions(quickTitles); // Quick pricing card document.getElementById('pricingStrategy').innerHTML = `

Preview Notes

${ocrResult ? `

✓ AI detected information from photos

` : `

⚠ Add API key in Settings for auto-detection

` }

This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.

${ocrResult ? `

Detected from photos:

` : ''}
`; // Simple fee floor const fees = suggestedPrice * 0.16; const safeFloor = Math.ceil(cost + fees + 6); document.getElementById('feeFloor').innerHTML = `

Your Cost

£${cost.toFixed(2)}

Est. Fees

£${fees.toFixed(2)}

Ship + Pack

£6.00

Safe Floor

£${safeFloor}

`; // Preview HTML description const previewHtml = `

${detectedArtist} - ${detectedTitle}

${year ? `

Year: ${year}

` : ''} ${catNo ? `

Catalogue #: ${catNo}

` : ''}

Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}


[Full description will be generated with complete market analysis]

`; const htmlOutput = document.getElementById('htmlOutput'); if (htmlOutput) htmlOutput.value = previewHtml; // Preview tags const previewTags = [ detectedArtist, detectedTitle, 'vinyl', 'record', vinylCond, 'lp', year || 'vintage' ].filter(Boolean); const tagsOutput = document.getElementById('tagsOutput'); if (tagsOutput) { tagsOutput.innerHTML = previewTags.map(t => ` ${t} `).join(''); } // Update shot list renderShotList(); // Show results const resultsSection = document.getElementById('resultsSection'); const emptyState = document.getElementById('emptyState'); if (resultsSection) resultsSection.classList.remove('hidden'); if (emptyState) emptyState.classList.add('hidden'); if (resultsSection) resultsSection.scrollIntoView({ behavior: 'smooth' }); showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success'); } catch (error) { console.error('Preview error:', error); showToast('Preview failed: ' + error.message, 'error'); } finally { stopAnalysisProgress(); setTimeout(() => { spinner.classList.add('hidden'); dropZone.classList.remove('pointer-events-none'); updateAnalysisProgress('Initializing...', 0); }, 300); } } async function callAI(messages, temperature = 0.7) { const provider = localStorage.getItem('ai_provider') || 'openai'; if (provider === 'deepseek' && window.deepseekService?.isConfigured) { try { const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}` }, body: JSON.stringify({ model: localStorage.getItem('deepseek_model') || 'deepseek-chat', messages: messages, temperature: temperature, max_tokens: 2000 }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'DeepSeek API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`DeepSeek Error: ${error.message}`, 'error'); return null; } } else { // Fallback to OpenAI const apiKey = localStorage.getItem('openai_api_key'); const model = localStorage.getItem('openai_model') || 'gpt-4o'; const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000; if (!apiKey) { showToast('OpenAI API key not configured. Go to Settings.', 'error'); return null; } try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: messages, temperature: temperature, max_tokens: maxTokens }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`OpenAI Error: ${error.message}`, 'error'); return null; } } } // Legacy alias for backward compatibility async function callOpenAI(messages, temperature = 0.7) { return callAI(messages, temperature); } // Delete hosted image from imgBB async function deleteHostedImage(deleteUrl) { if (!deleteUrl) return false; try { const response = await fetch(deleteUrl, { method: 'GET' }); // imgBB delete URLs work via GET request return response.ok; } catch (error) { console.error('Failed to delete image:', error); return false; } } // Get hosted photo URLs for eBay HTML description function getHostedPhotoUrlsForEbay() { return hostedPhotoUrls.map(img => ({ full: img.url, display: img.displayUrl || img.url, thumb: img.thumb, medium: img.medium, viewer: img.viewerUrl })); } async function generateListingWithAI() { const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); const catNo = document.getElementById('catInput').value.trim(); const year = document.getElementById('yearInput').value.trim(); if (!artist || !title) { showToast('Please enter at least artist and title', 'error'); return; } const messages = [ { role: 'system', content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).' }, { role: 'user', content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.` } ]; const provider = localStorage.getItem('ai_provider') || 'openai'; showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success'); const result = await callAI(messages, 0.7); if (result) { try { const data = JSON.parse(result); // Populate the UI with AI-generated content if (data.titles) { renderTitleOptions(data.titles.map(t => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: 'AI Generated' }))); } if (data.description) { document.getElementById('htmlOutput').value = data.description; } if (data.tags) { const tagsContainer = document.getElementById('tagsOutput'); tagsContainer.innerHTML = data.tags.map(t => ` ${t} `).join(''); } resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); showToast('AI listing generated!', 'success'); } catch (e) { // If not valid JSON, treat as plain text description document.getElementById('htmlOutput').value = result; resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); } } } function requestHelp() { alert(`VINYL PHOTO GUIDE: ESSENTIAL SHOTS (need these): • Front cover - square, no glare, color accurate • Back cover - full frame, readable text • Both labels - close enough to read all text • Deadwax/runout - for pressing identification CONDITION SHOTS: • Vinyl in raking light at angle (shows scratches) • Sleeve edges and corners • Any flaws clearly documented OPTIONARY BUT HELPFUL: • Inner sleeve condition • Inserts, posters, extras • Hype stickers • Barcode area TIPS: - Use natural daylight or 5500K bulbs - Avoid flash directly on glossy sleeves - Include scale reference if unusual size - Photograph flaws honestly - reduces returns`); } function showToast(message, type = 'success') { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const iconMap = { success: 'check', error: 'alert-circle', warning: 'alert-triangle' }; const colorMap = { success: 'text-green-400', error: 'text-red-400', warning: 'text-yellow-400' }; const toast = document.createElement('div'); toast.className = `toast ${type} flex items-center gap-3`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); feather.replace(); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // Cleanup function to delete all hosted images for current listing async function cleanupHostedImages() { if (window.currentListingImages) { for (const img of window.currentListingImages) { if (img.deleteUrl) { await deleteHostedImage(img.deleteUrl); } } window.currentListingImages = []; } } // Initialize document.addEventListener('DOMContentLoaded', () => { console.log('VinylVault Pro initialized'); // Initialize drop zone initDropZone(); // Warn about unsaved changes when leaving page with hosted images window.addEventListener('beforeunload', (e) => { if (hostedPhotoUrls.length > 0 && !window.listingPublished) { // Optional: could add cleanup here or warn user } }); }); : '€'; // Mock comp research results const comps = { nm: { low: 45, high: 65, median: 52 }, vgplus: { low: 28, high: 42, median: 34 }, vg: { low: 15, high: 25, median: 19 } }; // Calculate recommended price based on goal let recommendedBin, strategy; switch(goal) { case 'quick': recommendedBin = Math.round(comps.vgplus.low * 0.9); strategy = 'BIN + Best Offer (aggressive)'; break; case 'max': recommendedBin = Math.round(comps.nm.high * 1.1); strategy = 'BIN only, no offers, long duration'; break; default: recommendedBin = comps.vgplus.median; strategy = 'BIN + Best Offer (standard)'; } // Fee calculation (eBay UK approx) const ebayFeeRate = 0.13; // 13% final value fee const paypalRate = 0.029; // 2.9% + 30p const fixedFee = 0.30; const shippingCost = 4.50; // Estimated const packingCost = 1.50; const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee; const breakEven = cost + totalFees + shippingCost + packingCost; const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer // Generate titles const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`; const titles = generateTitles(baseTitle, catNo, year, goal); // Render results renderTitleOptions(titles); renderPricingStrategy(recommendedBin, strategy, comps, currency, goal); renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency); await renderHTMLDescription(data, titles[0]); renderTags(artist, title, catNo, year); renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); currentAnalysis = { titles, recommendedBin, strategy, breakEven, safeFloor, currency }; } function generateTitles(base, catNo, year, goal) { const titles = []; const cat = catNo || 'CAT#'; const yr = year || 'YEAR'; const country = window.detectedCountry || 'UK'; const genre = window.detectedGenre || 'Rock'; const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP'; // Option 1: Classic collector focus titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`); // Option 2: Condition forward titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`); // Option 3: Rarity/hype with detected genre titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`); // Option 4: Clean searchable titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`); // Option 5: Genre tagged titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`); return titles.map((t, i) => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i] })); } function renderTitleOptions(titles) { const container = document.getElementById('titleOptions'); container.innerHTML = titles.map((t, i) => `
${t.chars}/80

${t.text}

${t.style}

`).join(''); } function selectTitle(el, text) { document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); // Update clipboard copy navigator.clipboard.writeText(text); showToast('Title copied to clipboard!', 'success'); } function renderPricingStrategy(bin, strategy, comps, currency, goal) { const container = document.getElementById('pricingStrategy'); const offerSettings = goal === 'max' ? 'Offers: OFF' : `Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`; container.innerHTML = `

Sold Comps by Grade

NM/NM- ${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX ${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+ ${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})

Based on last 90 days sold listings, same pressing. Prices exclude postage.

`; } function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) { const container = document.getElementById('feeFloor'); container.innerHTML = `

Your Cost

${currency}${cost.toFixed(2)}

Est. Fees

${currency}${fees.toFixed(2)}

~16% total

Ship + Pack

${currency}${(shipping + packing).toFixed(2)}

Safe Floor Price

${currency}${safeFloor}

Auto-decline below this

`; } async function renderHTMLDescription(data, titleObj) { const { artist, title, catNo, year } = data; // Use hosted URL if available, otherwise fallback to local object URL let heroImg = ''; let galleryImages = []; if (hostedPhotoUrls.length > 0) { heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url; galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url); } else if (uploadedPhotos.length > 0) { heroImg = URL.createObjectURL(uploadedPhotos[0]); galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1])); } // Use OCR-detected values if available const detectedLabel = window.detectedLabel || '[Verify from photos]'; const detectedCountry = window.detectedCountry || 'UK'; const detectedFormat = window.detectedFormat || 'LP • 33rpm'; const detectedGenre = window.detectedGenre || 'rock'; const detectedCondition = window.detectedCondition || 'VG+/VG+'; const detectedPressingInfo = window.detectedPressingInfo || ''; // Fetch tracklist and detailed info from Discogs if available let tracklistHtml = ''; let pressingDetailsHtml = ''; let provenanceHtml = ''; if (window.discogsReleaseId && window.discogsService?.key) { try { const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId); if (discogsData && discogsData.tracklist) { // Build tracklist HTML const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B'))); if (hasSideBreakdown) { // Group by sides const sides = {}; discogsData.tracklist.forEach(track => { const side = track.position ? track.position.charAt(0) : 'Other'; if (!sides[side]) sides[side] = []; sides[side].push(track); }); tracklistHtml = Object.entries(sides).map(([side, tracks]) => `

Side ${side}

${tracks.map(track => `
${track.position} ${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`).join(''); } else { // Simple list tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`; } // Build pressing/variation details const identifiers = discogsData.identifiers || []; const barcodeInfo = identifiers.find(i => i.type === 'Barcode'); const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout'); const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering'); if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) { pressingDetailsHtml = `

Pressing & Matrix Information

${barcodeInfo ? `

Barcode: ${barcodeInfo.value}

` : ''} ${matrixInfo.map(m => `

${m.type}: ${m.value}${m.description ? ` (${m.description})` : ''}

`).join('')} ${pressingInfo.map(p => `

${p.type}: ${p.value}

`).join('')}
${discogsData.notes ? `

${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}

` : ''}
`; } // Build provenance data for buyer confidence const companies = discogsData.companies || []; const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering')); const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing')); const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At'); if (masteredBy || pressedBy || lacquerCut) { provenanceHtml = `

Provenance & Production

${masteredBy ? `

✓ Mastered at ${masteredBy.name}

` : ''} ${lacquerCut ? `

✓ Lacquer cut at ${lacquerCut.name}

` : ''} ${pressedBy ? `

✓ Pressed at ${pressedBy.name}

` : ''} ${discogsData.num_for_sale ? `

Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs

` : ''}
`; } } } catch (e) { console.error('Failed to fetch Discogs details for HTML:', e); } } // If no tracklist from Discogs, provide placeholder if (!tracklistHtml) { tracklistHtml = `

Tracklist verification recommended. Please compare with Discogs entry for accuracy.

`; } const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `Record photo`).join('')}
` : ''; const html = `
${artist} - ${title}
${galleryHtml}
Original ${detectedCountry} Pressing ${year || '1970s'} ${detectedFormat} ${detectedCondition}
Artist ${artist || 'See title'}
Title ${title || 'See title'}
Label ${detectedLabel}
Catalogue ${catNo || '[See photos]'}
Country ${detectedCountry}
Year ${year || '[Verify]'}

Condition Report

Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]

About This Release

${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]

Tracklist

${tracklistHtml}
${pressingDetailsHtml} ${provenanceHtml}

Packing & Postage

Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.

Combined postage: Discount available for multiple purchases—please request invoice before payment.

Questions? Need more photos?

Message me anytime—happy to provide additional angles, audio clips, or pressing details.

`; // Store reference to hosted images for potential cleanup window.currentListingImages = hostedPhotoUrls.map(img => ({ url: img.url, deleteUrl: img.deleteUrl })); document.getElementById('htmlOutput').value = html; } function renderTags(artist, title, catNo, year) { const genre = window.detectedGenre || 'rock'; const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' : window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp'; const country = window.detectedCountry?.toLowerCase() || 'uk'; const tags = [ artist || 'vinyl', title || 'record', format, 'vinyl record', 'original pressing', `${country} pressing`, year || 'vintage', catNo || '', genre, genre === 'rock' ? 'prog rock' : genre, genre === 'rock' ? 'psych' : '', 'collector', 'audiophile', format === 'lp' ? '12 inch' : format, '33 rpm', format === 'lp' ? 'album' : 'single', 'used vinyl', 'graded', 'excellent condition', 'rare vinyl', 'classic rock', 'vintage vinyl', 'record collection', 'music', 'audio', window.detectedLabel || '' ].filter(Boolean); const container = document.getElementById('tagsOutput'); container.innerHTML = tags.map(t => ` ${t} `).join(''); } function renderShotList() { // Map shot types to display info const shotDefinitions = [ { id: 'front', name: 'Front cover (square, well-lit)', critical: true }, { id: 'back', name: 'Back cover (full shot)', critical: true }, { id: 'spine', name: 'Spine (readable text)', critical: true }, { id: 'label_a', name: 'Label Side A (close, legible)', critical: true }, { id: 'label_b', name: 'Label Side B (close, legible)', critical: true }, { id: 'deadwax', name: 'Deadwax/runout grooves', critical: true }, { id: 'inner', name: 'Inner sleeve (both sides)', critical: false }, { id: 'insert', name: 'Insert/poster if included', critical: false }, { id: 'hype', name: 'Hype sticker (if present)', critical: false }, { id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true }, { id: 'corners', name: 'Sleeve corners/edges detail', critical: false }, { id: 'barcode', name: 'Barcode area', critical: false } ]; // Check if we have any photos at all const hasPhotos = uploadedPhotos.length > 0; const container = document.getElementById('shotList'); container.innerHTML = shotDefinitions.map(shot => { const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1); const statusClass = have ? 'completed' : shot.critical ? 'missing' : ''; const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500'; const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300'; const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle'; return `
${shot.name} ${shot.critical && !have ? 'CRITICAL' : ''}
`}).join(''); feather.replace(); } function copyHTML() { const html = document.getElementById('htmlOutput'); html.select(); document.execCommand('copy'); showToast('HTML copied to clipboard!', 'success'); } function copyTags() { const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', '); navigator.clipboard.writeText(tags); showToast('Tags copied to clipboard!', 'success'); } async function draftAnalysis() { if (uploadedPhotos.length === 0) { showToast('Upload photos first for preview', 'error'); return; } const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); // Show loading state const dropZone = document.getElementById('dropZone'); const spinner = document.getElementById('uploadSpinner'); spinner.classList.remove('hidden'); dropZone.classList.add('pointer-events-none'); startAnalysisProgressSimulation(); try { // Try OCR/AI analysis if available const service = getAIService(); let ocrResult = null; if (service && service.apiKey && uploadedPhotos.length > 0) { try { ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed populateFieldsFromOCR(ocrResult); } catch (e) { console.log('Preview OCR failed:', e); } } // Generate quick preview results const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || ''; const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || ''; const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist'; const detectedTitle = title || ocrResult?.title || 'Unknown Title'; const baseTitle = `${detectedArtist} - ${detectedTitle}`; // Generate quick titles const quickTitles = [ `${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80), `${baseTitle} Original Pressing Vinyl LP`.substring(0, 80), `${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80) ].map((t, i) => ({ text: t, chars: t.length, style: ['Quick', 'Standard', 'Compact'][i] })); // Quick pricing estimate based on condition const cost = parseFloat(document.getElementById('costInput').value) || 10; const vinylCond = document.getElementById('vinylConditionInput').value; const sleeveCond = document.getElementById('sleeveConditionInput').value; const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 }; const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3; const estimatedValue = Math.round(cost * Math.max(condMult, 1.5)); const suggestedPrice = Math.round(estimatedValue * 0.9); // Render preview results renderTitleOptions(quickTitles); // Quick pricing card document.getElementById('pricingStrategy').innerHTML = `

Preview Notes

${ocrResult ? `

✓ AI detected information from photos

` : `

⚠ Add API key in Settings for auto-detection

` }

This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.

${ocrResult ? `

Detected from photos:

` : ''}
`; // Simple fee floor const fees = suggestedPrice * 0.16; const safeFloor = Math.ceil(cost + fees + 6); document.getElementById('feeFloor').innerHTML = `

Your Cost

£${cost.toFixed(2)}

Est. Fees

£${fees.toFixed(2)}

Ship + Pack

£6.00

Safe Floor

£${safeFloor}

`; // Preview HTML description const previewHtml = `

${detectedArtist} - ${detectedTitle}

${year ? `

Year: ${year}

` : ''} ${catNo ? `

Catalogue #: ${catNo}

` : ''}

Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}


[Full description will be generated with complete market analysis]

`; document.getElementById('htmlOutput').value = previewHtml; // Preview tags const previewTags = [ detectedArtist, detectedTitle, 'vinyl', 'record', vinylCond, 'lp', year || 'vintage' ].filter(Boolean); document.getElementById('tagsOutput').innerHTML = previewTags.map(t => ` ${t} `).join(''); // Update shot list renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success'); } catch (error) { console.error('Preview error:', error); showToast('Preview failed: ' + error.message, 'error'); } finally { stopAnalysisProgress(); setTimeout(() => { spinner.classList.add('hidden'); dropZone.classList.remove('pointer-events-none'); updateAnalysisProgress('Initializing...', 0); }, 300); } } async function callAI(messages, temperature = 0.7) { const provider = localStorage.getItem('ai_provider') || 'openai'; if (provider === 'deepseek' && window.deepseekService?.isConfigured) { try { const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}` }, body: JSON.stringify({ model: localStorage.getItem('deepseek_model') || 'deepseek-chat', messages: messages, temperature: temperature, max_tokens: 2000 }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'DeepSeek API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`DeepSeek Error: ${error.message}`, 'error'); return null; } } else { // Fallback to OpenAI const apiKey = localStorage.getItem('openai_api_key'); const model = localStorage.getItem('openai_model') || 'gpt-4o'; const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000; if (!apiKey) { showToast('OpenAI API key not configured. Go to Settings.', 'error'); return null; } try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: messages, temperature: temperature, max_tokens: maxTokens }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`OpenAI Error: ${error.message}`, 'error'); return null; } } } // Legacy alias for backward compatibility async function callOpenAI(messages, temperature = 0.7) { return callAI(messages, temperature); } // Delete hosted image from imgBB async function deleteHostedImage(deleteUrl) { if (!deleteUrl) return false; try { const response = await fetch(deleteUrl, { method: 'GET' }); // imgBB delete URLs work via GET request return response.ok; } catch (error) { console.error('Failed to delete image:', error); return false; } } // Get hosted photo URLs for eBay HTML description function getHostedPhotoUrlsForEbay() { return hostedPhotoUrls.map(img => ({ full: img.url, display: img.displayUrl || img.url, thumb: img.thumb, medium: img.medium, viewer: img.viewerUrl })); } async function generateListingWithAI() { const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); const catNo = document.getElementById('catInput').value.trim(); const year = document.getElementById('yearInput').value.trim(); if (!artist || !title) { showToast('Please enter at least artist and title', 'error'); return; } const messages = [ { role: 'system', content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).' }, { role: 'user', content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.` } ]; const provider = localStorage.getItem('ai_provider') || 'openai'; showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success'); const result = await callAI(messages, 0.7); if (result) { try { const data = JSON.parse(result); // Populate the UI with AI-generated content if (data.titles) { renderTitleOptions(data.titles.map(t => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: 'AI Generated' }))); } if (data.description) { document.getElementById('htmlOutput').value = data.description; } if (data.tags) { const tagsContainer = document.getElementById('tagsOutput'); tagsContainer.innerHTML = data.tags.map(t => ` ${t} `).join(''); } resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); showToast('AI listing generated!', 'success'); } catch (e) { // If not valid JSON, treat as plain text description document.getElementById('htmlOutput').value = result; resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); } } } function requestHelp() { alert(`VINYL PHOTO GUIDE: ESSENTIAL SHOTS (need these): • Front cover - square, no glare, color accurate • Back cover - full frame, readable text • Both labels - close enough to read all text • Deadwax/runout - for pressing identification CONDITION SHOTS: • Vinyl in raking light at angle (shows scratches) • Sleeve edges and corners • Any flaws clearly documented OPTIONARY BUT HELPFUL: • Inner sleeve condition • Inserts, posters, extras • Hype stickers • Barcode area TIPS: - Use natural daylight or 5500K bulbs - Avoid flash directly on glossy sleeves - Include scale reference if unusual size - Photograph flaws honestly - reduces returns`); } function showToast(message, type = 'success') { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const iconMap = { success: 'check', error: 'alert-circle', warning: 'alert-triangle' }; const colorMap = { success: 'text-green-400', error: 'text-red-400', warning: 'text-yellow-400' }; const toast = document.createElement('div'); toast.className = `toast ${type} flex items-center gap-3`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); feather.replace(); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // Cleanup function to delete all hosted images for current listing async function cleanupHostedImages() { if (window.currentListingImages) { for (const img of window.currentListingImages) { if (img.deleteUrl) { await deleteHostedImage(img.deleteUrl); } } window.currentListingImages = []; } } // Initialize document.addEventListener('DOMContentLoaded', () => { console.log('VinylVault Pro initialized'); // Initialize drop zone initDropZone(); // Warn about unsaved changes when leaving page with hosted images window.addEventListener('beforeunload', (e) => { if (hostedPhotoUrls.length > 0 && !window.listingPublished) { // Optional: could add cleanup here or warn user } }); }); : '€'; // Mock comp research results const comps = { nm: { low: 45, high: 65, median: 52 }, vgplus: { low: 28, high: 42, median: 34 }, vg: { low: 15, high: 25, median: 19 } }; // Calculate recommended price based on goal let recommendedBin, strategy; switch(goal) { case 'quick': recommendedBin = Math.round(comps.vgplus.low * 0.9); strategy = 'BIN + Best Offer (aggressive)'; break; case 'max': recommendedBin = Math.round(comps.nm.high * 1.1); strategy = 'BIN only, no offers, long duration'; break; default: recommendedBin = comps.vgplus.median; strategy = 'BIN + Best Offer (standard)'; } // Fee calculation (eBay UK approx) const ebayFeeRate = 0.13; // 13% final value fee const paypalRate = 0.029; // 2.9% + 30p const fixedFee = 0.30; const shippingCost = 4.50; // Estimated const packingCost = 1.50; const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee; const breakEven = cost + totalFees + shippingCost + packingCost; const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer // Generate titles const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`; const titles = generateTitles(baseTitle, catNo, year, goal); // Render results renderTitleOptions(titles); renderPricingStrategy(recommendedBin, strategy, comps, currency, goal); renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency); await renderHTMLDescription(data, titles[0]); renderTags(artist, title, catNo, year); renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); currentAnalysis = { titles, recommendedBin, strategy, breakEven, safeFloor, currency }; } function renderTitleOptions(titles) { const container = document.getElementById('titleOptions'); container.innerHTML = titles.map((t, i) => `
${t.chars}/80

${t.text}

${t.style}

`).join(''); } function selectTitle(el, text) { document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); // Update clipboard copy navigator.clipboard.writeText(text); showToast('Title copied to clipboard!', 'success'); } function renderPricingStrategy(bin, strategy, comps, currency, goal) { const container = document.getElementById('pricingStrategy'); const offerSettings = goal === 'max' ? 'Offers: OFF' : `Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`; container.innerHTML = `

Sold Comps by Grade

NM/NM- ${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX ${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+ ${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})

Based on last 90 days sold listings, same pressing. Prices exclude postage.

`; } function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) { const container = document.getElementById('feeFloor'); container.innerHTML = `

Your Cost

${currency}${cost.toFixed(2)}

Est. Fees

${currency}${fees.toFixed(2)}

~16% total

Ship + Pack

${currency}${(shipping + packing).toFixed(2)}

Safe Floor Price

${currency}${safeFloor}

Auto-decline below this

`; } async function renderHTMLDescription(data, titleObj) { const { artist, title, catNo, year } = data; // Use hosted URL if available, otherwise fallback to local object URL let heroImg = ''; let galleryImages = []; if (hostedPhotoUrls.length > 0) { heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url; galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url); } else if (uploadedPhotos.length > 0) { heroImg = URL.createObjectURL(uploadedPhotos[0]); galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1])); } // Use OCR-detected values if available const detectedLabel = window.detectedLabel || '[Verify from photos]'; const detectedCountry = window.detectedCountry || 'UK'; const detectedFormat = window.detectedFormat || 'LP • 33rpm'; const detectedGenre = window.detectedGenre || 'rock'; const detectedCondition = window.detectedCondition || 'VG+/VG+'; const detectedPressingInfo = window.detectedPressingInfo || ''; // Fetch tracklist and detailed info from Discogs if available let tracklistHtml = ''; let pressingDetailsHtml = ''; let provenanceHtml = ''; if (window.discogsReleaseId && window.discogsService?.key) { try { const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId); if (discogsData && discogsData.tracklist) { // Build tracklist HTML const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B'))); if (hasSideBreakdown) { // Group by sides const sides = {}; discogsData.tracklist.forEach(track => { const side = track.position ? track.position.charAt(0) : 'Other'; if (!sides[side]) sides[side] = []; sides[side].push(track); }); tracklistHtml = Object.entries(sides).map(([side, tracks]) => `

Side ${side}

${tracks.map(track => `
${track.position} ${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`).join(''); } else { // Simple list tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`; } // Build pressing/variation details const identifiers = discogsData.identifiers || []; const barcodeInfo = identifiers.find(i => i.type === 'Barcode'); const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout'); const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering'); if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) { pressingDetailsHtml = `

Pressing & Matrix Information

${barcodeInfo ? `

Barcode: ${barcodeInfo.value}

` : ''} ${matrixInfo.map(m => `

${m.type}: ${m.value}${m.description ? ` (${m.description})` : ''}

`).join('')} ${pressingInfo.map(p => `

${p.type}: ${p.value}

`).join('')}
${discogsData.notes ? `

${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}

` : ''}
`; } // Build provenance data for buyer confidence const companies = discogsData.companies || []; const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering')); const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing')); const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At'); if (masteredBy || pressedBy || lacquerCut) { provenanceHtml = `

Provenance & Production

${masteredBy ? `

✓ Mastered at ${masteredBy.name}

` : ''} ${lacquerCut ? `

✓ Lacquer cut at ${lacquerCut.name}

` : ''} ${pressedBy ? `

✓ Pressed at ${pressedBy.name}

` : ''} ${discogsData.num_for_sale ? `

Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs

` : ''}
`; } } } catch (e) { console.error('Failed to fetch Discogs details for HTML:', e); } } // If no tracklist from Discogs, provide placeholder if (!tracklistHtml) { tracklistHtml = `

Tracklist verification recommended. Please compare with Discogs entry for accuracy.

`; } const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `Record photo`).join('')}
` : ''; const html = `
${artist} - ${title}
${galleryHtml}
Original ${detectedCountry} Pressing ${year || '1970s'} ${detectedFormat} ${detectedCondition}
Artist ${artist || 'See title'}
Title ${title || 'See title'}
Label ${detectedLabel}
Catalogue ${catNo || '[See photos]'}
Country ${detectedCountry}
Year ${year || '[Verify]'}

Condition Report

Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]

About This Release

${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]

Tracklist

${tracklistHtml}
${pressingDetailsHtml} ${provenanceHtml}

Packing & Postage

Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.

Combined postage: Discount available for multiple purchases—please request invoice before payment.

Questions? Need more photos?

Message me anytime—happy to provide additional angles, audio clips, or pressing details.

`; // Store reference to hosted images for potential cleanup window.currentListingImages = hostedPhotoUrls.map(img => ({ url: img.url, deleteUrl: img.deleteUrl })); document.getElementById('htmlOutput').value = html; } function renderTags(artist, title, catNo, year) { const genre = window.detectedGenre || 'rock'; const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' : window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp'; const country = window.detectedCountry?.toLowerCase() || 'uk'; const tags = [ artist || 'vinyl', title || 'record', format, 'vinyl record', 'original pressing', `${country} pressing`, year || 'vintage', catNo || '', genre, genre === 'rock' ? 'prog rock' : genre, genre === 'rock' ? 'psych' : '', 'collector', 'audiophile', format === 'lp' ? '12 inch' : format, '33 rpm', format === 'lp' ? 'album' : 'single', 'used vinyl', 'graded', 'excellent condition', 'rare vinyl', 'classic rock', 'vintage vinyl', 'record collection', 'music', 'audio', window.detectedLabel || '' ].filter(Boolean); const container = document.getElementById('tagsOutput'); container.innerHTML = tags.map(t => ` ${t} `).join(''); } function renderShotList() { // Map shot types to display info const shotDefinitions = [ { id: 'front', name: 'Front cover (square, well-lit)', critical: true }, { id: 'back', name: 'Back cover (full shot)', critical: true }, { id: 'spine', name: 'Spine (readable text)', critical: true }, { id: 'label_a', name: 'Label Side A (close, legible)', critical: true }, { id: 'label_b', name: 'Label Side B (close, legible)', critical: true }, { id: 'deadwax', name: 'Deadwax/runout grooves', critical: true }, { id: 'inner', name: 'Inner sleeve (both sides)', critical: false }, { id: 'insert', name: 'Insert/poster if included', critical: false }, { id: 'hype', name: 'Hype sticker (if present)', critical: false }, { id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true }, { id: 'corners', name: 'Sleeve corners/edges detail', critical: false }, { id: 'barcode', name: 'Barcode area', critical: false } ]; // Check if we have any photos at all const hasPhotos = uploadedPhotos.length > 0; const container = document.getElementById('shotList'); container.innerHTML = shotDefinitions.map(shot => { const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1); const statusClass = have ? 'completed' : shot.critical ? 'missing' : ''; const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500'; const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300'; const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle'; return `
${shot.name} ${shot.critical && !have ? 'CRITICAL' : ''}
`}).join(''); feather.replace(); } function copyHTML() { const html = document.getElementById('htmlOutput'); html.select(); document.execCommand('copy'); showToast('HTML copied to clipboard!', 'success'); } function copyTags() { const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', '); navigator.clipboard.writeText(tags); showToast('Tags copied to clipboard!', 'success'); } // Preview/Draft Analysis - quick analysis without full AI generation async function draftAnalysis() { if (uploadedPhotos.length === 0) { showToast('Upload photos first for preview', 'error'); return; } const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); // Show loading state const dropZone = document.getElementById('dropZone'); const spinner = document.getElementById('uploadSpinner'); spinner.classList.remove('hidden'); dropZone.classList.add('pointer-events-none'); startAnalysisProgressSimulation(); try { // Try OCR/AI analysis if available const service = getAIService(); let ocrResult = null; if (service && service.apiKey && uploadedPhotos.length > 0) { try { ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed populateFieldsFromOCR(ocrResult); } catch (e) { console.log('Preview OCR failed:', e); } } // Generate quick preview results const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || ''; const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || ''; const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist'; const detectedTitle = title || ocrResult?.title || 'Unknown Title'; const baseTitle = `${detectedArtist} - ${detectedTitle}`; // Generate quick titles const quickTitles = [ `${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80), `${baseTitle} Original Pressing Vinyl LP`.substring(0, 80), `${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80) ].map((t, i) => ({ text: t, chars: t.length, style: ['Quick', 'Standard', 'Compact'][i] })); // Quick pricing estimate based on condition const cost = parseFloat(document.getElementById('costInput').value) || 10; const vinylCond = document.getElementById('vinylConditionInput').value; const sleeveCond = document.getElementById('sleeveConditionInput').value; const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 }; const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3; const estimatedValue = Math.round(cost * Math.max(condMult, 1.5)); const suggestedPrice = Math.round(estimatedValue * 0.9); // Render preview results renderTitleOptions(quickTitles); // Quick pricing card document.getElementById('pricingStrategy').innerHTML = `

Preview Notes

${ocrResult ? `

✓ AI detected information from photos

` : `

⚠ Add API key in Settings for auto-detection

` }

This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.

${ocrResult ? `

Detected from photos:

` : ''}
`; // Simple fee floor const fees = suggestedPrice * 0.16; const safeFloor = Math.ceil(cost + fees + 6); document.getElementById('feeFloor').innerHTML = `

Your Cost

£${cost.toFixed(2)}

Est. Fees

£${fees.toFixed(2)}

Ship + Pack

£6.00

Safe Floor

£${safeFloor}

`; // Preview HTML description const previewHtml = `

${detectedArtist} - ${detectedTitle}

${year ? `

Year: ${year}

` : ''} ${catNo ? `

Catalogue #: ${catNo}

` : ''}

Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}


[Full description will be generated with complete market analysis]

`; const htmlOutput = document.getElementById('htmlOutput'); if (htmlOutput) htmlOutput.value = previewHtml; // Preview tags const previewTags = [ detectedArtist, detectedTitle, 'vinyl', 'record', vinylCond, 'lp', year || 'vintage' ].filter(Boolean); const tagsOutput = document.getElementById('tagsOutput'); if (tagsOutput) { tagsOutput.innerHTML = previewTags.map(t => ` ${t} `).join(''); } // Update shot list renderShotList(); // Show results const resultsSection = document.getElementById('resultsSection'); const emptyState = document.getElementById('emptyState'); if (resultsSection) resultsSection.classList.remove('hidden'); if (emptyState) emptyState.classList.add('hidden'); if (resultsSection) resultsSection.scrollIntoView({ behavior: 'smooth' }); showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success'); } catch (error) { console.error('Preview error:', error); showToast('Preview failed: ' + error.message, 'error'); } finally { stopAnalysisProgress(); setTimeout(() => { spinner.classList.add('hidden'); dropZone.classList.remove('pointer-events-none'); updateAnalysisProgress('Initializing...', 0); }, 300); } } async function callAI(messages, temperature = 0.7) { const provider = localStorage.getItem('ai_provider') || 'openai'; if (provider === 'deepseek' && window.deepseekService?.isConfigured) { try { const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}` }, body: JSON.stringify({ model: localStorage.getItem('deepseek_model') || 'deepseek-chat', messages: messages, temperature: temperature, max_tokens: 2000 }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'DeepSeek API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`DeepSeek Error: ${error.message}`, 'error'); return null; } } else { // Fallback to OpenAI const apiKey = localStorage.getItem('openai_api_key'); const model = localStorage.getItem('openai_model') || 'gpt-4o'; const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000; if (!apiKey) { showToast('OpenAI API key not configured. Go to Settings.', 'error'); return null; } try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: messages, temperature: temperature, max_tokens: maxTokens }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`OpenAI Error: ${error.message}`, 'error'); return null; } } } // Legacy alias for backward compatibility async function callOpenAI(messages, temperature = 0.7) { return callAI(messages, temperature); } // Delete hosted image from imgBB async function deleteHostedImage(deleteUrl) { if (!deleteUrl) return false; try { const response = await fetch(deleteUrl, { method: 'GET' }); // imgBB delete URLs work via GET request return response.ok; } catch (error) { console.error('Failed to delete image:', error); return false; } } // Get hosted photo URLs for eBay HTML description function getHostedPhotoUrlsForEbay() { return hostedPhotoUrls.map(img => ({ full: img.url, display: img.displayUrl || img.url, thumb: img.thumb, medium: img.medium, viewer: img.viewerUrl })); } async function generateListingWithAI() { const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); const catNo = document.getElementById('catInput').value.trim(); const year = document.getElementById('yearInput').value.trim(); if (!artist || !title) { showToast('Please enter at least artist and title', 'error'); return; } const messages = [ { role: 'system', content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).' }, { role: 'user', content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.` } ]; const provider = localStorage.getItem('ai_provider') || 'openai'; showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success'); const result = await callAI(messages, 0.7); if (result) { try { const data = JSON.parse(result); // Populate the UI with AI-generated content if (data.titles) { renderTitleOptions(data.titles.map(t => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: 'AI Generated' }))); } if (data.description) { document.getElementById('htmlOutput').value = data.description; } if (data.tags) { const tagsContainer = document.getElementById('tagsOutput'); tagsContainer.innerHTML = data.tags.map(t => ` ${t} `).join(''); } resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); showToast('AI listing generated!', 'success'); } catch (e) { // If not valid JSON, treat as plain text description document.getElementById('htmlOutput').value = result; resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); } } } function requestHelp() { alert(`VINYL PHOTO GUIDE: ESSENTIAL SHOTS (need these): • Front cover - square, no glare, color accurate • Back cover - full frame, readable text • Both labels - close enough to read all text • Deadwax/runout - for pressing identification CONDITION SHOTS: • Vinyl in raking light at angle (shows scratches) • Sleeve edges and corners • Any flaws clearly documented OPTIONARY BUT HELPFUL: • Inner sleeve condition • Inserts, posters, extras • Hype stickers • Barcode area TIPS: - Use natural daylight or 5500K bulbs - Avoid flash directly on glossy sleeves - Include scale reference if unusual size - Photograph flaws honestly - reduces returns`); } function showToast(message, type = 'success') { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const iconMap = { success: 'check', error: 'alert-circle', warning: 'alert-triangle' }; const colorMap = { success: 'text-green-400', error: 'text-red-400', warning: 'text-yellow-400' }; const toast = document.createElement('div'); toast.className = `toast ${type} flex items-center gap-3`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); feather.replace(); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // Cleanup function to delete all hosted images for current listing async function cleanupHostedImages() { if (window.currentListingImages) { for (const img of window.currentListingImages) { if (img.deleteUrl) { await deleteHostedImage(img.deleteUrl); } } window.currentListingImages = []; } } // Initialize document.addEventListener('DOMContentLoaded', () => { console.log('VinylVault Pro initialized'); // Initialize drop zone initDropZone(); // Attach event listeners to buttons const generateBtn = document.getElementById('generateListingBtn'); if (generateBtn) { generateBtn.addEventListener('click', generateListing); } const draftBtn = document.getElementById('draftAnalysisBtn'); if (draftBtn) { draftBtn.addEventListener('click', draftAnalysis); } const helpBtn = document.getElementById('requestHelpBtn'); if (helpBtn) { helpBtn.addEventListener('click', requestHelp); } const copyHTMLBtn = document.getElementById('copyHTMLBtn'); if (copyHTMLBtn) { copyHTMLBtn.addEventListener('click', copyHTML); } const copyTagsBtn = document.getElementById('copyTagsBtn'); if (copyTagsBtn) { copyTagsBtn.addEventListener('click', copyTags); } const analyzePhotoBtn = document.getElementById('analyzePhotoTypesBtn'); if (analyzePhotoBtn) { analyzePhotoBtn.addEventListener('click', analyzePhotoTypes); } // Clear Collection Import Banner listeners const clearCollectionBtn = document.querySelector('#collectionBanner button'); if (clearCollectionBtn) { clearCollectionBtn.addEventListener('click', clearCollectionImport); } // Warn about unsaved changes when leaving page with hosted images window.addEventListener('beforeunload', (e) => { if (hostedPhotoUrls.length > 0 && !window.listingPublished) { // Optional: could add cleanup here or warn user } }); }); // Collection Import functions (defined here to avoid reference errors) function clearCollectionImport() { sessionStorage.removeItem('collectionListingRecord'); const banner = document.getElementById('collectionBanner'); if (banner) { banner.classList.add('hidden'); } showToast('Collection import cleared', 'success'); } function checkCollectionImport() { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('fromCollection') === 'true') { const recordData = sessionStorage.getItem('collectionListingRecord'); if (recordData) { const record = JSON.parse(recordData); populateFieldsFromCollection(record); const banner = document.getElementById('collectionBanner'); if (banner) { banner.classList.remove('hidden'); } const indicator = document.getElementById('collectionDataIndicator'); if (indicator) { indicator.classList.remove('hidden'); } } } } function populateFieldsFromCollection(record) { if (!record) return; const fields = { 'artistInput': record.artist, 'titleInput': record.title, 'catInput': record.catalogueNumber || record.matrixNotes, 'yearInput': record.year, 'costInput': record.purchasePrice, 'daysOwnedInput': record.daysOwned }; Object.entries(fields).forEach(([fieldId, value]) => { const field = document.getElementById(fieldId); if (field && value) { field.value = value; } }); // Set conditions if available if (record.conditionVinyl) { const vinylCondition = document.getElementById('vinylConditionInput'); if (vinylCondition) vinylCondition.value = record.conditionVinyl; } if (record.conditionSleeve) { const sleeveCondition = document.getElementById('sleeveConditionInput'); if (sleeveCondition) sleeveCondition.value = record.conditionSleeve; } showToast(`Loaded ${record.artist} - ${record.title} from collection`, 'success'); } // Call check on load checkCollectionImport(); : '€'; // Mock comp research results const comps = { nm: { low: 45, high: 65, median: 52 }, vgplus: { low: 28, high: 42, median: 34 }, vg: { low: 15, high: 25, median: 19 } }; // Calculate recommended price based on goal let recommendedBin, strategy; switch(goal) { case 'quick': recommendedBin = Math.round(comps.vgplus.low * 0.9); strategy = 'BIN + Best Offer (aggressive)'; break; case 'max': recommendedBin = Math.round(comps.nm.high * 1.1); strategy = 'BIN only, no offers, long duration'; break; default: recommendedBin = comps.vgplus.median; strategy = 'BIN + Best Offer (standard)'; } // Fee calculation (eBay UK approx) const ebayFeeRate = 0.13; // 13% final value fee const paypalRate = 0.029; // 2.9% + 30p const fixedFee = 0.30; const shippingCost = 4.50; // Estimated const packingCost = 1.50; const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee; const breakEven = cost + totalFees + shippingCost + packingCost; const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer // Generate titles const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`; const titles = generateTitles(baseTitle, catNo, year, goal); // Render results renderTitleOptions(titles); renderPricingStrategy(recommendedBin, strategy, comps, currency, goal); renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency); await renderHTMLDescription(data, titles[0]); renderTags(artist, title, catNo, year); renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); currentAnalysis = { titles, recommendedBin, strategy, breakEven, safeFloor, currency }; } function generateTitles(base, catNo, year, goal) { const titles = []; const cat = catNo || 'CAT#'; const yr = year || 'YEAR'; const country = window.detectedCountry || 'UK'; const genre = window.detectedGenre || 'Rock'; const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP'; // Option 1: Classic collector focus titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`); // Option 2: Condition forward titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`); // Option 3: Rarity/hype with detected genre titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`); // Option 4: Clean searchable titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`); // Option 5: Genre tagged titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`); return titles.map((t, i) => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i] })); } function renderTitleOptions(titles) { const container = document.getElementById('titleOptions'); container.innerHTML = titles.map((t, i) => `
${t.chars}/80

${t.text}

${t.style}

`).join(''); } function selectTitle(el, text) { document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); // Update clipboard copy navigator.clipboard.writeText(text); showToast('Title copied to clipboard!', 'success'); } function renderPricingStrategy(bin, strategy, comps, currency, goal) { const container = document.getElementById('pricingStrategy'); const offerSettings = goal === 'max' ? 'Offers: OFF' : `Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`; container.innerHTML = `

Sold Comps by Grade

NM/NM- ${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX ${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+ ${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})

Based on last 90 days sold listings, same pressing. Prices exclude postage.

`; } function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) { const container = document.getElementById('feeFloor'); container.innerHTML = `

Your Cost

${currency}${cost.toFixed(2)}

Est. Fees

${currency}${fees.toFixed(2)}

~16% total

Ship + Pack

${currency}${(shipping + packing).toFixed(2)}

Safe Floor Price

${currency}${safeFloor}

Auto-decline below this

`; } async function renderHTMLDescription(data, titleObj) { const { artist, title, catNo, year } = data; // Use hosted URL if available, otherwise fallback to local object URL let heroImg = ''; let galleryImages = []; if (hostedPhotoUrls.length > 0) { heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url; galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url); } else if (uploadedPhotos.length > 0) { heroImg = URL.createObjectURL(uploadedPhotos[0]); galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1])); } // Use OCR-detected values if available const detectedLabel = window.detectedLabel || '[Verify from photos]'; const detectedCountry = window.detectedCountry || 'UK'; const detectedFormat = window.detectedFormat || 'LP • 33rpm'; const detectedGenre = window.detectedGenre || 'rock'; const detectedCondition = window.detectedCondition || 'VG+/VG+'; const detectedPressingInfo = window.detectedPressingInfo || ''; // Fetch tracklist and detailed info from Discogs if available let tracklistHtml = ''; let pressingDetailsHtml = ''; let provenanceHtml = ''; if (window.discogsReleaseId && window.discogsService?.key) { try { const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId); if (discogsData && discogsData.tracklist) { // Build tracklist HTML const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B'))); if (hasSideBreakdown) { // Group by sides const sides = {}; discogsData.tracklist.forEach(track => { const side = track.position ? track.position.charAt(0) : 'Other'; if (!sides[side]) sides[side] = []; sides[side].push(track); }); tracklistHtml = Object.entries(sides).map(([side, tracks]) => `

Side ${side}

${tracks.map(track => `
${track.position} ${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`).join(''); } else { // Simple list tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`; } // Build pressing/variation details const identifiers = discogsData.identifiers || []; const barcodeInfo = identifiers.find(i => i.type === 'Barcode'); const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout'); const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering'); if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) { pressingDetailsHtml = `

Pressing & Matrix Information

${barcodeInfo ? `

Barcode: ${barcodeInfo.value}

` : ''} ${matrixInfo.map(m => `

${m.type}: ${m.value}${m.description ? ` (${m.description})` : ''}

`).join('')} ${pressingInfo.map(p => `

${p.type}: ${p.value}

`).join('')}
${discogsData.notes ? `

${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}

` : ''}
`; } // Build provenance data for buyer confidence const companies = discogsData.companies || []; const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering')); const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing')); const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At'); if (masteredBy || pressedBy || lacquerCut) { provenanceHtml = `

Provenance & Production

${masteredBy ? `

✓ Mastered at ${masteredBy.name}

` : ''} ${lacquerCut ? `

✓ Lacquer cut at ${lacquerCut.name}

` : ''} ${pressedBy ? `

✓ Pressed at ${pressedBy.name}

` : ''} ${discogsData.num_for_sale ? `

Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs

` : ''}
`; } } } catch (e) { console.error('Failed to fetch Discogs details for HTML:', e); } } // If no tracklist from Discogs, provide placeholder if (!tracklistHtml) { tracklistHtml = `

Tracklist verification recommended. Please compare with Discogs entry for accuracy.

`; } const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `Record photo`).join('')}
` : ''; const html = `
${artist} - ${title}
${galleryHtml}
Original ${detectedCountry} Pressing ${year || '1970s'} ${detectedFormat} ${detectedCondition}
Artist ${artist || 'See title'}
Title ${title || 'See title'}
Label ${detectedLabel}
Catalogue ${catNo || '[See photos]'}
Country ${detectedCountry}
Year ${year || '[Verify]'}

Condition Report

Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]

About This Release

${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]

Tracklist

${tracklistHtml}
${pressingDetailsHtml} ${provenanceHtml}

Packing & Postage

Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.

Combined postage: Discount available for multiple purchases—please request invoice before payment.

Questions? Need more photos?

Message me anytime—happy to provide additional angles, audio clips, or pressing details.

`; // Store reference to hosted images for potential cleanup window.currentListingImages = hostedPhotoUrls.map(img => ({ url: img.url, deleteUrl: img.deleteUrl })); document.getElementById('htmlOutput').value = html; } function renderTags(artist, title, catNo, year) { const genre = window.detectedGenre || 'rock'; const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' : window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp'; const country = window.detectedCountry?.toLowerCase() || 'uk'; const tags = [ artist || 'vinyl', title || 'record', format, 'vinyl record', 'original pressing', `${country} pressing`, year || 'vintage', catNo || '', genre, genre === 'rock' ? 'prog rock' : genre, genre === 'rock' ? 'psych' : '', 'collector', 'audiophile', format === 'lp' ? '12 inch' : format, '33 rpm', format === 'lp' ? 'album' : 'single', 'used vinyl', 'graded', 'excellent condition', 'rare vinyl', 'classic rock', 'vintage vinyl', 'record collection', 'music', 'audio', window.detectedLabel || '' ].filter(Boolean); const container = document.getElementById('tagsOutput'); container.innerHTML = tags.map(t => ` ${t} `).join(''); } function renderShotList() { // Map shot types to display info const shotDefinitions = [ { id: 'front', name: 'Front cover (square, well-lit)', critical: true }, { id: 'back', name: 'Back cover (full shot)', critical: true }, { id: 'spine', name: 'Spine (readable text)', critical: true }, { id: 'label_a', name: 'Label Side A (close, legible)', critical: true }, { id: 'label_b', name: 'Label Side B (close, legible)', critical: true }, { id: 'deadwax', name: 'Deadwax/runout grooves', critical: true }, { id: 'inner', name: 'Inner sleeve (both sides)', critical: false }, { id: 'insert', name: 'Insert/poster if included', critical: false }, { id: 'hype', name: 'Hype sticker (if present)', critical: false }, { id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true }, { id: 'corners', name: 'Sleeve corners/edges detail', critical: false }, { id: 'barcode', name: 'Barcode area', critical: false } ]; // Check if we have any photos at all const hasPhotos = uploadedPhotos.length > 0; const container = document.getElementById('shotList'); container.innerHTML = shotDefinitions.map(shot => { const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1); const statusClass = have ? 'completed' : shot.critical ? 'missing' : ''; const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500'; const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300'; const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle'; return `
${shot.name} ${shot.critical && !have ? 'CRITICAL' : ''}
`}).join(''); feather.replace(); } function copyHTML() { const html = document.getElementById('htmlOutput'); html.select(); document.execCommand('copy'); showToast('HTML copied to clipboard!', 'success'); } function copyTags() { const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', '); navigator.clipboard.writeText(tags); showToast('Tags copied to clipboard!', 'success'); } async function draftAnalysis() { if (uploadedPhotos.length === 0) { showToast('Upload photos first for preview', 'error'); return; } const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); // Show loading state const dropZone = document.getElementById('dropZone'); const spinner = document.getElementById('uploadSpinner'); spinner.classList.remove('hidden'); dropZone.classList.add('pointer-events-none'); startAnalysisProgressSimulation(); try { // Try OCR/AI analysis if available const service = getAIService(); let ocrResult = null; if (service && service.apiKey && uploadedPhotos.length > 0) { try { ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed populateFieldsFromOCR(ocrResult); } catch (e) { console.log('Preview OCR failed:', e); } } // Generate quick preview results const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || ''; const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || ''; const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist'; const detectedTitle = title || ocrResult?.title || 'Unknown Title'; const baseTitle = `${detectedArtist} - ${detectedTitle}`; // Generate quick titles const quickTitles = [ `${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80), `${baseTitle} Original Pressing Vinyl LP`.substring(0, 80), `${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80) ].map((t, i) => ({ text: t, chars: t.length, style: ['Quick', 'Standard', 'Compact'][i] })); // Quick pricing estimate based on condition const cost = parseFloat(document.getElementById('costInput').value) || 10; const vinylCond = document.getElementById('vinylConditionInput').value; const sleeveCond = document.getElementById('sleeveConditionInput').value; const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 }; const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3; const estimatedValue = Math.round(cost * Math.max(condMult, 1.5)); const suggestedPrice = Math.round(estimatedValue * 0.9); // Render preview results renderTitleOptions(quickTitles); // Quick pricing card document.getElementById('pricingStrategy').innerHTML = `

Preview Notes

${ocrResult ? `

✓ AI detected information from photos

` : `

⚠ Add API key in Settings for auto-detection

` }

This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.

${ocrResult ? `

Detected from photos:

` : ''}
`; // Simple fee floor const fees = suggestedPrice * 0.16; const safeFloor = Math.ceil(cost + fees + 6); document.getElementById('feeFloor').innerHTML = `

Your Cost

£${cost.toFixed(2)}

Est. Fees

£${fees.toFixed(2)}

Ship + Pack

£6.00

Safe Floor

£${safeFloor}

`; // Preview HTML description const previewHtml = `

${detectedArtist} - ${detectedTitle}

${year ? `

Year: ${year}

` : ''} ${catNo ? `

Catalogue #: ${catNo}

` : ''}

Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}


[Full description will be generated with complete market analysis]

`; document.getElementById('htmlOutput').value = previewHtml; // Preview tags const previewTags = [ detectedArtist, detectedTitle, 'vinyl', 'record', vinylCond, 'lp', year || 'vintage' ].filter(Boolean); document.getElementById('tagsOutput').innerHTML = previewTags.map(t => ` ${t} `).join(''); // Update shot list renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success'); } catch (error) { console.error('Preview error:', error); showToast('Preview failed: ' + error.message, 'error'); } finally { stopAnalysisProgress(); setTimeout(() => { spinner.classList.add('hidden'); dropZone.classList.remove('pointer-events-none'); updateAnalysisProgress('Initializing...', 0); }, 300); } } async function callAI(messages, temperature = 0.7) { const provider = localStorage.getItem('ai_provider') || 'openai'; if (provider === 'deepseek' && window.deepseekService?.isConfigured) { try { const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}` }, body: JSON.stringify({ model: localStorage.getItem('deepseek_model') || 'deepseek-chat', messages: messages, temperature: temperature, max_tokens: 2000 }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'DeepSeek API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`DeepSeek Error: ${error.message}`, 'error'); return null; } } else { // Fallback to OpenAI const apiKey = localStorage.getItem('openai_api_key'); const model = localStorage.getItem('openai_model') || 'gpt-4o'; const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000; if (!apiKey) { showToast('OpenAI API key not configured. Go to Settings.', 'error'); return null; } try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: messages, temperature: temperature, max_tokens: maxTokens }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`OpenAI Error: ${error.message}`, 'error'); return null; } } } // Legacy alias for backward compatibility async function callOpenAI(messages, temperature = 0.7) { return callAI(messages, temperature); } // Delete hosted image from imgBB async function deleteHostedImage(deleteUrl) { if (!deleteUrl) return false; try { const response = await fetch(deleteUrl, { method: 'GET' }); // imgBB delete URLs work via GET request return response.ok; } catch (error) { console.error('Failed to delete image:', error); return false; } } // Get hosted photo URLs for eBay HTML description function getHostedPhotoUrlsForEbay() { return hostedPhotoUrls.map(img => ({ full: img.url, display: img.displayUrl || img.url, thumb: img.thumb, medium: img.medium, viewer: img.viewerUrl })); } async function generateListingWithAI() { const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); const catNo = document.getElementById('catInput').value.trim(); const year = document.getElementById('yearInput').value.trim(); if (!artist || !title) { showToast('Please enter at least artist and title', 'error'); return; } const messages = [ { role: 'system', content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).' }, { role: 'user', content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.` } ]; const provider = localStorage.getItem('ai_provider') || 'openai'; showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success'); const result = await callAI(messages, 0.7); if (result) { try { const data = JSON.parse(result); // Populate the UI with AI-generated content if (data.titles) { renderTitleOptions(data.titles.map(t => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: 'AI Generated' }))); } if (data.description) { document.getElementById('htmlOutput').value = data.description; } if (data.tags) { const tagsContainer = document.getElementById('tagsOutput'); tagsContainer.innerHTML = data.tags.map(t => ` ${t} `).join(''); } resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); showToast('AI listing generated!', 'success'); } catch (e) { // If not valid JSON, treat as plain text description document.getElementById('htmlOutput').value = result; resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); } } } function requestHelp() { alert(`VINYL PHOTO GUIDE: ESSENTIAL SHOTS (need these): • Front cover - square, no glare, color accurate • Back cover - full frame, readable text • Both labels - close enough to read all text • Deadwax/runout - for pressing identification CONDITION SHOTS: • Vinyl in raking light at angle (shows scratches) • Sleeve edges and corners • Any flaws clearly documented OPTIONARY BUT HELPFUL: • Inner sleeve condition • Inserts, posters, extras • Hype stickers • Barcode area TIPS: - Use natural daylight or 5500K bulbs - Avoid flash directly on glossy sleeves - Include scale reference if unusual size - Photograph flaws honestly - reduces returns`); } function showToast(message, type = 'success') { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const iconMap = { success: 'check', error: 'alert-circle', warning: 'alert-triangle' }; const colorMap = { success: 'text-green-400', error: 'text-red-400', warning: 'text-yellow-400' }; const toast = document.createElement('div'); toast.className = `toast ${type} flex items-center gap-3`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); feather.replace(); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // Cleanup function to delete all hosted images for current listing async function cleanupHostedImages() { if (window.currentListingImages) { for (const img of window.currentListingImages) { if (img.deleteUrl) { await deleteHostedImage(img.deleteUrl); } } window.currentListingImages = []; } } // Initialize document.addEventListener('DOMContentLoaded', () => { console.log('VinylVault Pro initialized'); // Initialize drop zone initDropZone(); // Warn about unsaved changes when leaving page with hosted images window.addEventListener('beforeunload', (e) => { if (hostedPhotoUrls.length > 0 && !window.listingPublished) { // Optional: could add cleanup here or warn user } }); }); : '€'; // Mock comp research results const comps = { nm: { low: 45, high: 65, median: 52 }, vgplus: { low: 28, high: 42, median: 34 }, vg: { low: 15, high: 25, median: 19 } }; // Calculate recommended price based on goal let recommendedBin, strategy; switch(goal) { case 'quick': recommendedBin = Math.round(comps.vgplus.low * 0.9); strategy = 'BIN + Best Offer (aggressive)'; break; case 'max': recommendedBin = Math.round(comps.nm.high * 1.1); strategy = 'BIN only, no offers, long duration'; break; default: recommendedBin = comps.vgplus.median; strategy = 'BIN + Best Offer (standard)'; } // Fee calculation (eBay UK approx) const ebayFeeRate = 0.13; // 13% final value fee const paypalRate = 0.029; // 2.9% + 30p const fixedFee = 0.30; const shippingCost = 4.50; // Estimated const packingCost = 1.50; const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee; const breakEven = cost + totalFees + shippingCost + packingCost; const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer // Generate titles const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`; const titles = generateTitles(baseTitle, catNo, year, goal); // Render results renderTitleOptions(titles); renderPricingStrategy(recommendedBin, strategy, comps, currency, goal); renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency); await renderHTMLDescription(data, titles[0]); renderTags(artist, title, catNo, year); renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); currentAnalysis = { titles, recommendedBin, strategy, breakEven, safeFloor, currency }; } function generateTitles(base, catNo, year, goal) { const titles = []; const cat = catNo || 'CAT#'; const yr = year || 'YEAR'; const country = window.detectedCountry || 'UK'; const genre = window.detectedGenre || 'Rock'; const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP'; // Option 1: Classic collector focus titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`); // Option 2: Condition forward titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`); // Option 3: Rarity/hype with detected genre titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`); // Option 4: Clean searchable titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`); // Option 5: Genre tagged titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`); return titles.map((t, i) => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i] })); } function renderTitleOptions(titles) { const container = document.getElementById('titleOptions'); container.innerHTML = titles.map((t, i) => `
${t.chars}/80

${t.text}

${t.style}

`).join(''); } function selectTitle(el, text) { document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); // Update clipboard copy navigator.clipboard.writeText(text); showToast('Title copied to clipboard!', 'success'); } function renderPricingStrategy(bin, strategy, comps, currency, goal) { const container = document.getElementById('pricingStrategy'); const offerSettings = goal === 'max' ? 'Offers: OFF' : `Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`; container.innerHTML = `

Sold Comps by Grade

NM/NM- ${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX ${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+ ${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})

Based on last 90 days sold listings, same pressing. Prices exclude postage.

`; } function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) { const container = document.getElementById('feeFloor'); container.innerHTML = `

Your Cost

${currency}${cost.toFixed(2)}

Est. Fees

${currency}${fees.toFixed(2)}

~16% total

Ship + Pack

${currency}${(shipping + packing).toFixed(2)}

Safe Floor Price

${currency}${safeFloor}

Auto-decline below this

`; } async function renderHTMLDescription(data, titleObj) { const { artist, title, catNo, year } = data; // Use hosted URL if available, otherwise fallback to local object URL let heroImg = ''; let galleryImages = []; if (hostedPhotoUrls.length > 0) { heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url; galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url); } else if (uploadedPhotos.length > 0) { heroImg = URL.createObjectURL(uploadedPhotos[0]); galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1])); } // Use OCR-detected values if available const detectedLabel = window.detectedLabel || '[Verify from photos]'; const detectedCountry = window.detectedCountry || 'UK'; const detectedFormat = window.detectedFormat || 'LP • 33rpm'; const detectedGenre = window.detectedGenre || 'rock'; const detectedCondition = window.detectedCondition || 'VG+/VG+'; const detectedPressingInfo = window.detectedPressingInfo || ''; // Fetch tracklist and detailed info from Discogs if available let tracklistHtml = ''; let pressingDetailsHtml = ''; let provenanceHtml = ''; if (window.discogsReleaseId && window.discogsService?.key) { try { const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId); if (discogsData && discogsData.tracklist) { // Build tracklist HTML const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B'))); if (hasSideBreakdown) { // Group by sides const sides = {}; discogsData.tracklist.forEach(track => { const side = track.position ? track.position.charAt(0) : 'Other'; if (!sides[side]) sides[side] = []; sides[side].push(track); }); tracklistHtml = Object.entries(sides).map(([side, tracks]) => `

Side ${side}

${tracks.map(track => `
${track.position} ${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`).join(''); } else { // Simple list tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`; } // Build pressing/variation details const identifiers = discogsData.identifiers || []; const barcodeInfo = identifiers.find(i => i.type === 'Barcode'); const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout'); const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering'); if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) { pressingDetailsHtml = `

Pressing & Matrix Information

${barcodeInfo ? `

Barcode: ${barcodeInfo.value}

` : ''} ${matrixInfo.map(m => `

${m.type}: ${m.value}${m.description ? ` (${m.description})` : ''}

`).join('')} ${pressingInfo.map(p => `

${p.type}: ${p.value}

`).join('')}
${discogsData.notes ? `

${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}

` : ''}
`; } // Build provenance data for buyer confidence const companies = discogsData.companies || []; const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering')); const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing')); const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At'); if (masteredBy || pressedBy || lacquerCut) { provenanceHtml = `

Provenance & Production

${masteredBy ? `

✓ Mastered at ${masteredBy.name}

` : ''} ${lacquerCut ? `

✓ Lacquer cut at ${lacquerCut.name}

` : ''} ${pressedBy ? `

✓ Pressed at ${pressedBy.name}

` : ''} ${discogsData.num_for_sale ? `

Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs

` : ''}
`; } } } catch (e) { console.error('Failed to fetch Discogs details for HTML:', e); } } // If no tracklist from Discogs, provide placeholder if (!tracklistHtml) { tracklistHtml = `

Tracklist verification recommended. Please compare with Discogs entry for accuracy.

`; } const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `Record photo`).join('')}
` : ''; const html = `
${artist} - ${title}
${galleryHtml}
Original ${detectedCountry} Pressing ${year || '1970s'} ${detectedFormat} ${detectedCondition}
Artist ${artist || 'See title'}
Title ${title || 'See title'}
Label ${detectedLabel}
Catalogue ${catNo || '[See photos]'}
Country ${detectedCountry}
Year ${year || '[Verify]'}

Condition Report

Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]

About This Release

${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]

Tracklist

${tracklistHtml}
${pressingDetailsHtml} ${provenanceHtml}

Packing & Postage

Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.

Combined postage: Discount available for multiple purchases—please request invoice before payment.

Questions? Need more photos?

Message me anytime—happy to provide additional angles, audio clips, or pressing details.

`; // Store reference to hosted images for potential cleanup window.currentListingImages = hostedPhotoUrls.map(img => ({ url: img.url, deleteUrl: img.deleteUrl })); document.getElementById('htmlOutput').value = html; } function renderTags(artist, title, catNo, year) { const genre = window.detectedGenre || 'rock'; const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' : window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp'; const country = window.detectedCountry?.toLowerCase() || 'uk'; const tags = [ artist || 'vinyl', title || 'record', format, 'vinyl record', 'original pressing', `${country} pressing`, year || 'vintage', catNo || '', genre, genre === 'rock' ? 'prog rock' : genre, genre === 'rock' ? 'psych' : '', 'collector', 'audiophile', format === 'lp' ? '12 inch' : format, '33 rpm', format === 'lp' ? 'album' : 'single', 'used vinyl', 'graded', 'excellent condition', 'rare vinyl', 'classic rock', 'vintage vinyl', 'record collection', 'music', 'audio', window.detectedLabel || '' ].filter(Boolean); const container = document.getElementById('tagsOutput'); container.innerHTML = tags.map(t => ` ${t} `).join(''); } function renderShotList() { // Map shot types to display info const shotDefinitions = [ { id: 'front', name: 'Front cover (square, well-lit)', critical: true }, { id: 'back', name: 'Back cover (full shot)', critical: true }, { id: 'spine', name: 'Spine (readable text)', critical: true }, { id: 'label_a', name: 'Label Side A (close, legible)', critical: true }, { id: 'label_b', name: 'Label Side B (close, legible)', critical: true }, { id: 'deadwax', name: 'Deadwax/runout grooves', critical: true }, { id: 'inner', name: 'Inner sleeve (both sides)', critical: false }, { id: 'insert', name: 'Insert/poster if included', critical: false }, { id: 'hype', name: 'Hype sticker (if present)', critical: false }, { id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true }, { id: 'corners', name: 'Sleeve corners/edges detail', critical: false }, { id: 'barcode', name: 'Barcode area', critical: false } ]; // Check if we have any photos at all const hasPhotos = uploadedPhotos.length > 0; const container = document.getElementById('shotList'); container.innerHTML = shotDefinitions.map(shot => { const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1); const statusClass = have ? 'completed' : shot.critical ? 'missing' : ''; const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500'; const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300'; const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle'; return `
${shot.name} ${shot.critical && !have ? 'CRITICAL' : ''}
`}).join(''); feather.replace(); } function copyHTML() { const html = document.getElementById('htmlOutput'); html.select(); document.execCommand('copy'); showToast('HTML copied to clipboard!', 'success'); } function copyTags() { const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', '); navigator.clipboard.writeText(tags); showToast('Tags copied to clipboard!', 'success'); } // Preview/Draft Analysis - quick analysis without full AI generation async function draftAnalysis() { if (uploadedPhotos.length === 0) { showToast('Upload photos first for preview', 'error'); return; } const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); // Show loading state const dropZone = document.getElementById('dropZone'); const spinner = document.getElementById('uploadSpinner'); spinner.classList.remove('hidden'); dropZone.classList.add('pointer-events-none'); startAnalysisProgressSimulation(); try { // Try OCR/AI analysis if available const service = getAIService(); let ocrResult = null; if (service && service.apiKey && uploadedPhotos.length > 0) { try { ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed populateFieldsFromOCR(ocrResult); } catch (e) { console.log('Preview OCR failed:', e); } } // Generate quick preview results const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || ''; const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || ''; const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist'; const detectedTitle = title || ocrResult?.title || 'Unknown Title'; const baseTitle = `${detectedArtist} - ${detectedTitle}`; // Generate quick titles const quickTitles = [ `${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80), `${baseTitle} Original Pressing Vinyl LP`.substring(0, 80), `${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80) ].map((t, i) => ({ text: t, chars: t.length, style: ['Quick', 'Standard', 'Compact'][i] })); // Quick pricing estimate based on condition const cost = parseFloat(document.getElementById('costInput').value) || 10; const vinylCond = document.getElementById('vinylConditionInput').value; const sleeveCond = document.getElementById('sleeveConditionInput').value; const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 }; const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3; const estimatedValue = Math.round(cost * Math.max(condMult, 1.5)); const suggestedPrice = Math.round(estimatedValue * 0.9); // Render preview results renderTitleOptions(quickTitles); // Quick pricing card document.getElementById('pricingStrategy').innerHTML = `

Preview Notes

${ocrResult ? `

✓ AI detected information from photos

` : `

⚠ Add API key in Settings for auto-detection

` }

This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.

${ocrResult ? `

Detected from photos:

` : ''}
`; // Simple fee floor const fees = suggestedPrice * 0.16; const safeFloor = Math.ceil(cost + fees + 6); document.getElementById('feeFloor').innerHTML = `

Your Cost

£${cost.toFixed(2)}

Est. Fees

£${fees.toFixed(2)}

Ship + Pack

£6.00

Safe Floor

£${safeFloor}

`; // Preview HTML description const previewHtml = `

${detectedArtist} - ${detectedTitle}

${year ? `

Year: ${year}

` : ''} ${catNo ? `

Catalogue #: ${catNo}

` : ''}

Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}


[Full description will be generated with complete market analysis]

`; const htmlOutput = document.getElementById('htmlOutput'); if (htmlOutput) htmlOutput.value = previewHtml; // Preview tags const previewTags = [ detectedArtist, detectedTitle, 'vinyl', 'record', vinylCond, 'lp', year || 'vintage' ].filter(Boolean); const tagsOutput = document.getElementById('tagsOutput'); if (tagsOutput) { tagsOutput.innerHTML = previewTags.map(t => ` ${t} `).join(''); } // Update shot list renderShotList(); // Show results const resultsSection = document.getElementById('resultsSection'); const emptyState = document.getElementById('emptyState'); if (resultsSection) resultsSection.classList.remove('hidden'); if (emptyState) emptyState.classList.add('hidden'); if (resultsSection) resultsSection.scrollIntoView({ behavior: 'smooth' }); showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success'); } catch (error) { console.error('Preview error:', error); showToast('Preview failed: ' + error.message, 'error'); } finally { stopAnalysisProgress(); setTimeout(() => { spinner.classList.add('hidden'); dropZone.classList.remove('pointer-events-none'); updateAnalysisProgress('Initializing...', 0); }, 300); } } async function callAI(messages, temperature = 0.7) { const provider = localStorage.getItem('ai_provider') || 'openai'; if (provider === 'deepseek' && window.deepseekService?.isConfigured) { try { const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}` }, body: JSON.stringify({ model: localStorage.getItem('deepseek_model') || 'deepseek-chat', messages: messages, temperature: temperature, max_tokens: 2000 }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'DeepSeek API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`DeepSeek Error: ${error.message}`, 'error'); return null; } } else { // Fallback to OpenAI const apiKey = localStorage.getItem('openai_api_key'); const model = localStorage.getItem('openai_model') || 'gpt-4o'; const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000; if (!apiKey) { showToast('OpenAI API key not configured. Go to Settings.', 'error'); return null; } try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: messages, temperature: temperature, max_tokens: maxTokens }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`OpenAI Error: ${error.message}`, 'error'); return null; } } } // Legacy alias for backward compatibility async function callOpenAI(messages, temperature = 0.7) { return callAI(messages, temperature); } // Delete hosted image from imgBB async function deleteHostedImage(deleteUrl) { if (!deleteUrl) return false; try { const response = await fetch(deleteUrl, { method: 'GET' }); // imgBB delete URLs work via GET request return response.ok; } catch (error) { console.error('Failed to delete image:', error); return false; } } // Get hosted photo URLs for eBay HTML description function getHostedPhotoUrlsForEbay() { return hostedPhotoUrls.map(img => ({ full: img.url, display: img.displayUrl || img.url, thumb: img.thumb, medium: img.medium, viewer: img.viewerUrl })); } async function generateListingWithAI() { const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); const catNo = document.getElementById('catInput').value.trim(); const year = document.getElementById('yearInput').value.trim(); if (!artist || !title) { showToast('Please enter at least artist and title', 'error'); return; } const messages = [ { role: 'system', content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).' }, { role: 'user', content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.` } ]; const provider = localStorage.getItem('ai_provider') || 'openai'; showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success'); const result = await callAI(messages, 0.7); if (result) { try { const data = JSON.parse(result); // Populate the UI with AI-generated content if (data.titles) { renderTitleOptions(data.titles.map(t => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: 'AI Generated' }))); } if (data.description) { document.getElementById('htmlOutput').value = data.description; } if (data.tags) { const tagsContainer = document.getElementById('tagsOutput'); tagsContainer.innerHTML = data.tags.map(t => ` ${t} `).join(''); } resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); showToast('AI listing generated!', 'success'); } catch (e) { // If not valid JSON, treat as plain text description document.getElementById('htmlOutput').value = result; resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); } } } function requestHelp() { alert(`VINYL PHOTO GUIDE: ESSENTIAL SHOTS (need these): • Front cover - square, no glare, color accurate • Back cover - full frame, readable text • Both labels - close enough to read all text • Deadwax/runout - for pressing identification CONDITION SHOTS: • Vinyl in raking light at angle (shows scratches) • Sleeve edges and corners • Any flaws clearly documented OPTIONARY BUT HELPFUL: • Inner sleeve condition • Inserts, posters, extras • Hype stickers • Barcode area TIPS: - Use natural daylight or 5500K bulbs - Avoid flash directly on glossy sleeves - Include scale reference if unusual size - Photograph flaws honestly - reduces returns`); } function showToast(message, type = 'success') { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const iconMap = { success: 'check', error: 'alert-circle', warning: 'alert-triangle' }; const colorMap = { success: 'text-green-400', error: 'text-red-400', warning: 'text-yellow-400' }; const toast = document.createElement('div'); toast.className = `toast ${type} flex items-center gap-3`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); feather.replace(); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // Cleanup function to delete all hosted images for current listing async function cleanupHostedImages() { if (window.currentListingImages) { for (const img of window.currentListingImages) { if (img.deleteUrl) { await deleteHostedImage(img.deleteUrl); } } window.currentListingImages = []; } } // Initialize document.addEventListener('DOMContentLoaded', () => { console.log('VinylVault Pro initialized'); // Initialize drop zone initDropZone(); // Warn about unsaved changes when leaving page with hosted images window.addEventListener('beforeunload', (e) => { if (hostedPhotoUrls.length > 0 && !window.listingPublished) { // Optional: could add cleanup here or warn user } }); }); : '€'; // Mock comp research results const comps = { nm: { low: 45, high: 65, median: 52 }, vgplus: { low: 28, high: 42, median: 34 }, vg: { low: 15, high: 25, median: 19 } }; // Calculate recommended price based on goal let recommendedBin, strategy; switch(goal) { case 'quick': recommendedBin = Math.round(comps.vgplus.low * 0.9); strategy = 'BIN + Best Offer (aggressive)'; break; case 'max': recommendedBin = Math.round(comps.nm.high * 1.1); strategy = 'BIN only, no offers, long duration'; break; default: recommendedBin = comps.vgplus.median; strategy = 'BIN + Best Offer (standard)'; } // Fee calculation (eBay UK approx) const ebayFeeRate = 0.13; // 13% final value fee const paypalRate = 0.029; // 2.9% + 30p const fixedFee = 0.30; const shippingCost = 4.50; // Estimated const packingCost = 1.50; const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee; const breakEven = cost + totalFees + shippingCost + packingCost; const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer // Generate titles const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`; const titles = generateTitles(baseTitle, catNo, year, goal); // Render results renderTitleOptions(titles); renderPricingStrategy(recommendedBin, strategy, comps, currency, goal); renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency); await renderHTMLDescription(data, titles[0]); renderTags(artist, title, catNo, year); renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); currentAnalysis = { titles, recommendedBin, strategy, breakEven, safeFloor, currency }; } function generateTitles(base, catNo, year, goal) { const titles = []; const cat = catNo || 'CAT#'; const yr = year || 'YEAR'; const country = window.detectedCountry || 'UK'; const genre = window.detectedGenre || 'Rock'; const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP'; // Option 1: Classic collector focus titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`); // Option 2: Condition forward titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`); // Option 3: Rarity/hype with detected genre titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`); // Option 4: Clean searchable titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`); // Option 5: Genre tagged titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`); return titles.map((t, i) => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i] })); } function renderTitleOptions(titles) { const container = document.getElementById('titleOptions'); container.innerHTML = titles.map((t, i) => `
${t.chars}/80

${t.text}

${t.style}

`).join(''); } function selectTitle(el, text) { document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected')); el.classList.add('selected'); // Update clipboard copy navigator.clipboard.writeText(text); showToast('Title copied to clipboard!', 'success'); } function renderPricingStrategy(bin, strategy, comps, currency, goal) { const container = document.getElementById('pricingStrategy'); const offerSettings = goal === 'max' ? 'Offers: OFF' : `Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`; container.innerHTML = `

Sold Comps by Grade

NM/NM- ${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX ${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+ ${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})

Based on last 90 days sold listings, same pressing. Prices exclude postage.

`; } function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) { const container = document.getElementById('feeFloor'); container.innerHTML = `

Your Cost

${currency}${cost.toFixed(2)}

Est. Fees

${currency}${fees.toFixed(2)}

~16% total

Ship + Pack

${currency}${(shipping + packing).toFixed(2)}

Safe Floor Price

${currency}${safeFloor}

Auto-decline below this

`; } async function renderHTMLDescription(data, titleObj) { const { artist, title, catNo, year } = data; // Use hosted URL if available, otherwise fallback to local object URL let heroImg = ''; let galleryImages = []; if (hostedPhotoUrls.length > 0) { heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url; galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url); } else if (uploadedPhotos.length > 0) { heroImg = URL.createObjectURL(uploadedPhotos[0]); galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1])); } // Use OCR-detected values if available const detectedLabel = window.detectedLabel || '[Verify from photos]'; const detectedCountry = window.detectedCountry || 'UK'; const detectedFormat = window.detectedFormat || 'LP • 33rpm'; const detectedGenre = window.detectedGenre || 'rock'; const detectedCondition = window.detectedCondition || 'VG+/VG+'; const detectedPressingInfo = window.detectedPressingInfo || ''; // Fetch tracklist and detailed info from Discogs if available let tracklistHtml = ''; let pressingDetailsHtml = ''; let provenanceHtml = ''; if (window.discogsReleaseId && window.discogsService?.key) { try { const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId); if (discogsData && discogsData.tracklist) { // Build tracklist HTML const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B'))); if (hasSideBreakdown) { // Group by sides const sides = {}; discogsData.tracklist.forEach(track => { const side = track.position ? track.position.charAt(0) : 'Other'; if (!sides[side]) sides[side] = []; sides[side].push(track); }); tracklistHtml = Object.entries(sides).map(([side, tracks]) => `

Side ${side}

${tracks.map(track => `
${track.position} ${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`).join(''); } else { // Simple list tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title} ${track.duration ? `${track.duration}` : ''}
`).join('')}
`; } // Build pressing/variation details const identifiers = discogsData.identifiers || []; const barcodeInfo = identifiers.find(i => i.type === 'Barcode'); const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout'); const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering'); if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) { pressingDetailsHtml = `

Pressing & Matrix Information

${barcodeInfo ? `

Barcode: ${barcodeInfo.value}

` : ''} ${matrixInfo.map(m => `

${m.type}: ${m.value}${m.description ? ` (${m.description})` : ''}

`).join('')} ${pressingInfo.map(p => `

${p.type}: ${p.value}

`).join('')}
${discogsData.notes ? `

${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}

` : ''}
`; } // Build provenance data for buyer confidence const companies = discogsData.companies || []; const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering')); const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing')); const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At'); if (masteredBy || pressedBy || lacquerCut) { provenanceHtml = `

Provenance & Production

${masteredBy ? `

✓ Mastered at ${masteredBy.name}

` : ''} ${lacquerCut ? `

✓ Lacquer cut at ${lacquerCut.name}

` : ''} ${pressedBy ? `

✓ Pressed at ${pressedBy.name}

` : ''} ${discogsData.num_for_sale ? `

Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs

` : ''}
`; } } } catch (e) { console.error('Failed to fetch Discogs details for HTML:', e); } } // If no tracklist from Discogs, provide placeholder if (!tracklistHtml) { tracklistHtml = `

Tracklist verification recommended. Please compare with Discogs entry for accuracy.

`; } const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `Record photo`).join('')}
` : ''; const html = `
${artist} - ${title}
${galleryHtml}
Original ${detectedCountry} Pressing ${year || '1970s'} ${detectedFormat} ${detectedCondition}
Artist ${artist || 'See title'}
Title ${title || 'See title'}
Label ${detectedLabel}
Catalogue ${catNo || '[See photos]'}
Country ${detectedCountry}
Year ${year || '[Verify]'}

Condition Report

Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]

About This Release

${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]

Tracklist

${tracklistHtml}
${pressingDetailsHtml} ${provenanceHtml}

Packing & Postage

Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.

Combined postage: Discount available for multiple purchases—please request invoice before payment.

Questions? Need more photos?

Message me anytime—happy to provide additional angles, audio clips, or pressing details.

`; // Store reference to hosted images for potential cleanup window.currentListingImages = hostedPhotoUrls.map(img => ({ url: img.url, deleteUrl: img.deleteUrl })); document.getElementById('htmlOutput').value = html; } function renderTags(artist, title, catNo, year) { const genre = window.detectedGenre || 'rock'; const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' : window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp'; const country = window.detectedCountry?.toLowerCase() || 'uk'; const tags = [ artist || 'vinyl', title || 'record', format, 'vinyl record', 'original pressing', `${country} pressing`, year || 'vintage', catNo || '', genre, genre === 'rock' ? 'prog rock' : genre, genre === 'rock' ? 'psych' : '', 'collector', 'audiophile', format === 'lp' ? '12 inch' : format, '33 rpm', format === 'lp' ? 'album' : 'single', 'used vinyl', 'graded', 'excellent condition', 'rare vinyl', 'classic rock', 'vintage vinyl', 'record collection', 'music', 'audio', window.detectedLabel || '' ].filter(Boolean); const container = document.getElementById('tagsOutput'); container.innerHTML = tags.map(t => ` ${t} `).join(''); } function renderShotList() { // Map shot types to display info const shotDefinitions = [ { id: 'front', name: 'Front cover (square, well-lit)', critical: true }, { id: 'back', name: 'Back cover (full shot)', critical: true }, { id: 'spine', name: 'Spine (readable text)', critical: true }, { id: 'label_a', name: 'Label Side A (close, legible)', critical: true }, { id: 'label_b', name: 'Label Side B (close, legible)', critical: true }, { id: 'deadwax', name: 'Deadwax/runout grooves', critical: true }, { id: 'inner', name: 'Inner sleeve (both sides)', critical: false }, { id: 'insert', name: 'Insert/poster if included', critical: false }, { id: 'hype', name: 'Hype sticker (if present)', critical: false }, { id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true }, { id: 'corners', name: 'Sleeve corners/edges detail', critical: false }, { id: 'barcode', name: 'Barcode area', critical: false } ]; // Check if we have any photos at all const hasPhotos = uploadedPhotos.length > 0; const container = document.getElementById('shotList'); container.innerHTML = shotDefinitions.map(shot => { const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1); const statusClass = have ? 'completed' : shot.critical ? 'missing' : ''; const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500'; const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300'; const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle'; return `
${shot.name} ${shot.critical && !have ? 'CRITICAL' : ''}
`}).join(''); feather.replace(); } function copyHTML() { const html = document.getElementById('htmlOutput'); html.select(); document.execCommand('copy'); showToast('HTML copied to clipboard!', 'success'); } function copyTags() { const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', '); navigator.clipboard.writeText(tags); showToast('Tags copied to clipboard!', 'success'); } async function draftAnalysis() { if (uploadedPhotos.length === 0) { showToast('Upload photos first for preview', 'error'); return; } const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); // Show loading state const dropZone = document.getElementById('dropZone'); const spinner = document.getElementById('uploadSpinner'); spinner.classList.remove('hidden'); dropZone.classList.add('pointer-events-none'); startAnalysisProgressSimulation(); try { // Try OCR/AI analysis if available const service = getAIService(); let ocrResult = null; if (service && service.apiKey && uploadedPhotos.length > 0) { try { ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed populateFieldsFromOCR(ocrResult); } catch (e) { console.log('Preview OCR failed:', e); } } // Generate quick preview results const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || ''; const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || ''; const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist'; const detectedTitle = title || ocrResult?.title || 'Unknown Title'; const baseTitle = `${detectedArtist} - ${detectedTitle}`; // Generate quick titles const quickTitles = [ `${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80), `${baseTitle} Original Pressing Vinyl LP`.substring(0, 80), `${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80) ].map((t, i) => ({ text: t, chars: t.length, style: ['Quick', 'Standard', 'Compact'][i] })); // Quick pricing estimate based on condition const cost = parseFloat(document.getElementById('costInput').value) || 10; const vinylCond = document.getElementById('vinylConditionInput').value; const sleeveCond = document.getElementById('sleeveConditionInput').value; const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 }; const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3; const estimatedValue = Math.round(cost * Math.max(condMult, 1.5)); const suggestedPrice = Math.round(estimatedValue * 0.9); // Render preview results renderTitleOptions(quickTitles); // Quick pricing card document.getElementById('pricingStrategy').innerHTML = `

Preview Notes

${ocrResult ? `

✓ AI detected information from photos

` : `

⚠ Add API key in Settings for auto-detection

` }

This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.

${ocrResult ? `

Detected from photos:

` : ''}
`; // Simple fee floor const fees = suggestedPrice * 0.16; const safeFloor = Math.ceil(cost + fees + 6); document.getElementById('feeFloor').innerHTML = `

Your Cost

£${cost.toFixed(2)}

Est. Fees

£${fees.toFixed(2)}

Ship + Pack

£6.00

Safe Floor

£${safeFloor}

`; // Preview HTML description const previewHtml = `

${detectedArtist} - ${detectedTitle}

${year ? `

Year: ${year}

` : ''} ${catNo ? `

Catalogue #: ${catNo}

` : ''}

Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}


[Full description will be generated with complete market analysis]

`; document.getElementById('htmlOutput').value = previewHtml; // Preview tags const previewTags = [ detectedArtist, detectedTitle, 'vinyl', 'record', vinylCond, 'lp', year || 'vintage' ].filter(Boolean); document.getElementById('tagsOutput').innerHTML = previewTags.map(t => ` ${t} `).join(''); // Update shot list renderShotList(); // Show results resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); resultsSection.scrollIntoView({ behavior: 'smooth' }); showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success'); } catch (error) { console.error('Preview error:', error); showToast('Preview failed: ' + error.message, 'error'); } finally { stopAnalysisProgress(); setTimeout(() => { spinner.classList.add('hidden'); dropZone.classList.remove('pointer-events-none'); updateAnalysisProgress('Initializing...', 0); }, 300); } } async function callAI(messages, temperature = 0.7) { const provider = localStorage.getItem('ai_provider') || 'openai'; if (provider === 'deepseek' && window.deepseekService?.isConfigured) { try { const response = await fetch('https://api.deepseek.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}` }, body: JSON.stringify({ model: localStorage.getItem('deepseek_model') || 'deepseek-chat', messages: messages, temperature: temperature, max_tokens: 2000 }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'DeepSeek API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`DeepSeek Error: ${error.message}`, 'error'); return null; } } else { // Fallback to OpenAI const apiKey = localStorage.getItem('openai_api_key'); const model = localStorage.getItem('openai_model') || 'gpt-4o'; const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000; if (!apiKey) { showToast('OpenAI API key not configured. Go to Settings.', 'error'); return null; } try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model, messages: messages, temperature: temperature, max_tokens: maxTokens }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || 'API request failed'); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { showToast(`OpenAI Error: ${error.message}`, 'error'); return null; } } } // Legacy alias for backward compatibility async function callOpenAI(messages, temperature = 0.7) { return callAI(messages, temperature); } // Delete hosted image from imgBB async function deleteHostedImage(deleteUrl) { if (!deleteUrl) return false; try { const response = await fetch(deleteUrl, { method: 'GET' }); // imgBB delete URLs work via GET request return response.ok; } catch (error) { console.error('Failed to delete image:', error); return false; } } // Get hosted photo URLs for eBay HTML description function getHostedPhotoUrlsForEbay() { return hostedPhotoUrls.map(img => ({ full: img.url, display: img.displayUrl || img.url, thumb: img.thumb, medium: img.medium, viewer: img.viewerUrl })); } async function generateListingWithAI() { const artist = document.getElementById('artistInput').value.trim(); const title = document.getElementById('titleInput').value.trim(); const catNo = document.getElementById('catInput').value.trim(); const year = document.getElementById('yearInput').value.trim(); if (!artist || !title) { showToast('Please enter at least artist and title', 'error'); return; } const messages = [ { role: 'system', content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).' }, { role: 'user', content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.` } ]; const provider = localStorage.getItem('ai_provider') || 'openai'; showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success'); const result = await callAI(messages, 0.7); if (result) { try { const data = JSON.parse(result); // Populate the UI with AI-generated content if (data.titles) { renderTitleOptions(data.titles.map(t => ({ text: t.length > 80 ? t.substring(0, 77) + '...' : t, chars: Math.min(t.length, 80), style: 'AI Generated' }))); } if (data.description) { document.getElementById('htmlOutput').value = data.description; } if (data.tags) { const tagsContainer = document.getElementById('tagsOutput'); tagsContainer.innerHTML = data.tags.map(t => ` ${t} `).join(''); } resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); showToast('AI listing generated!', 'success'); } catch (e) { // If not valid JSON, treat as plain text description document.getElementById('htmlOutput').value = result; resultsSection.classList.remove('hidden'); emptyState.classList.add('hidden'); } } } function requestHelp() { alert(`VINYL PHOTO GUIDE: ESSENTIAL SHOTS (need these): • Front cover - square, no glare, color accurate • Back cover - full frame, readable text • Both labels - close enough to read all text • Deadwax/runout - for pressing identification CONDITION SHOTS: • Vinyl in raking light at angle (shows scratches) • Sleeve edges and corners • Any flaws clearly documented OPTIONARY BUT HELPFUL: • Inner sleeve condition • Inserts, posters, extras • Hype stickers • Barcode area TIPS: - Use natural daylight or 5500K bulbs - Avoid flash directly on glossy sleeves - Include scale reference if unusual size - Photograph flaws honestly - reduces returns`); } function showToast(message, type = 'success') { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const iconMap = { success: 'check', error: 'alert-circle', warning: 'alert-triangle' }; const colorMap = { success: 'text-green-400', error: 'text-red-400', warning: 'text-yellow-400' }; const toast = document.createElement('div'); toast.className = `toast ${type} flex items-center gap-3`; toast.innerHTML = ` ${message} `; document.body.appendChild(toast); feather.replace(); requestAnimationFrame(() => toast.classList.add('show')); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // Cleanup function to delete all hosted images for current listing async function cleanupHostedImages() { if (window.currentListingImages) { for (const img of window.currentListingImages) { if (img.deleteUrl) { await deleteHostedImage(img.deleteUrl); } } window.currentListingImages = []; } } // Initialize document.addEventListener('DOMContentLoaded', () => { console.log('VinylVault Pro initialized'); // Initialize drop zone initDropZone(); // Warn about unsaved changes when leaving page with hosted images window.addEventListener('beforeunload', (e) => { if (hostedPhotoUrls.length > 0 && !window.listingPublished) { // Optional: could add cleanup here or warn user } }); });