Malachite is a tool to import your Last.fm and Spotify listening history to the AT Protocol network using the fm.teal.alpha.feed.play lexicon.
malachite scrobbles importer atproto music
at development 157 lines 4.7 kB view raw
1/** 2 * Utility functions for the Last.fm importer 3 */ 4import type { Config } from '../types.js'; 5 6/** 7 * Get user's locale from environment or default to system locale 8 */ 9function getUserLocale(): string { 10 // Try to get locale from environment variables 11 const envLang = process.env.LANG?.split('.')[0] || 12 process.env.LC_ALL?.split('.')[0]; 13 14 // Filter out invalid locales (like "C" or "POSIX") 15 if (envLang && envLang !== 'C' && envLang !== 'POSIX') { 16 // FIX: Replace underscore with hyphen to satisfy BCP 47 (e.g., en_GB -> en-GB) [cite: 413, 414] 17 return envLang.replace('_', '-'); 18 } 19 20 // Try system locale 21 try { 22 const systemLocale = Intl.DateTimeFormat().resolvedOptions().locale; 23 if (systemLocale && systemLocale !== 'C') { 24 return systemLocale; 25 } 26 } catch (e) { 27 // Ignore errors 28 } 29 30 // Default to UK format 31 return 'en-GB'; 32} 33 34/** 35 * Format a date in a locale-aware way 36 * @param date - Date string or Date object 37 * @param includeTime - Whether to include time in the output 38 * @returns Formatted date string 39 */ 40export function formatDate(date: string | Date, includeTime: boolean = false): string { 41 const dateObj = typeof date === 'string' ? new Date(date) : date; 42 const locale = getUserLocale(); 43 44 if (includeTime) { 45 return dateObj.toLocaleString(locale, { 46 year: 'numeric', 47 month: 'short', 48 day: 'numeric', 49 hour: '2-digit', 50 minute: '2-digit', 51 timeZoneName: 'short' 52 }); 53 } else { 54 return dateObj.toLocaleDateString(locale, { 55 year: 'numeric', 56 month: 'short', 57 day: 'numeric' 58 }); 59 } 60} 61 62/** 63 * Format a date range in a locale-aware way 64 * @param startDate - Start date 65 * @param endDate - End date 66 * @returns Formatted date range string 67 */ 68export function formatDateRange(startDate: string | Date, endDate: string | Date): string { 69 return `${formatDate(startDate)} to ${formatDate(endDate)}`; 70} 71 72/** 73 * Format duration in human-readable format 74 */ 75export function formatDuration(milliseconds: number): string { 76 const seconds = Math.floor(milliseconds / 1000); 77 const minutes = Math.floor(seconds / 60); 78 const hours = Math.floor(minutes / 60); 79 80 if (hours > 0) { 81 const mins = minutes % 60; 82 return `${hours}h ${mins}m`; 83 } else if (minutes > 0) { 84 const secs = seconds % 60; 85 return `${minutes}m ${secs}s`; 86 } else { 87 return `${seconds}s`; 88 } 89} 90 91/** 92 * Calculate optimal batch size based on total records and rate limits 93 * Uses a logarithmic scaling approach to balance throughput with API safety 94 */ 95export function calculateOptimalBatchSize(totalRecords: number, batchDelay: number, config: Config): number { 96 const { 97 MIN_RECORDS_FOR_SCALING, 98 BASE_BATCH_SIZE, 99 MAX_BATCH_SIZE, 100 SCALING_FACTOR, 101 DEFAULT_BATCH_DELAY 102 } = config; 103 104 const delay = batchDelay || DEFAULT_BATCH_DELAY; 105 106 // For very small datasets, use minimal batches 107 if (totalRecords <= 50) { 108 return 3; 109 } 110 111 // For small to medium datasets, use conservative batching 112 if (totalRecords <= MIN_RECORDS_FOR_SCALING) { 113 return BASE_BATCH_SIZE; 114 } 115 116 // Logarithmic scaling 117 const logScale = Math.log2(totalRecords / MIN_RECORDS_FOR_SCALING); 118 const calculatedSize = Math.floor(BASE_BATCH_SIZE + (logScale * SCALING_FACTOR)); 119 120 // Apply maximum cap 121 let optimalSize = Math.min(calculatedSize, MAX_BATCH_SIZE); 122 123 // Adjust based on batch delay 124 if (delay < 1500 && optimalSize > 15) { 125 optimalSize = Math.floor(optimalSize * 0.75); 126 } 127 128 // Ensure batch size is at least 3 129 return Math.max(3, optimalSize); 130} 131 132/** 133 * Logs rate limiting and batching information to the console. 134 * Note: This function cannot import log from logger.ts to avoid circular dependencies, 135 * so it uses console.log directly. The CLI controls the log level, so this output 136 * is appropriately controlled. 137 */ 138export function showRateLimitInfo( 139 totalRecords: number, 140 batchSize: number, 141 batchDelay: number, 142 estimatedDays: number, 143 dailyLimit: number 144): void { 145 console.log('\n📊 Rate Limiting Information:'); 146 console.log(` Total records: ${totalRecords.toLocaleString()}`); 147 console.log(` Daily limit: ${dailyLimit.toLocaleString()} records/day`); 148 console.log(` Estimated duration: ${estimatedDays} day${estimatedDays > 1 ? 's' : ''}`); 149 console.log(` Batch size: ${batchSize} records`); 150 console.log(` Batch delay: ${(batchDelay / 1000).toFixed(1)}s`); 151 152 if (estimatedDays > 1) { 153 console.log('\n The import will automatically pause between days.'); 154 console.log(' You can safely close and restart the importer - it will resume from where it left off.'); 155 } 156 console.log(''); 157}