// Name: Organize Downloads to Markdown // Description: Lists files in your Downloads folder, grouped by type, and saves a human-readable Markdown report. // Author: johnlindquist // GitHub: johnlindquist import '@johnlindquist/kit' type Entry = { name: string filePath: string size: number mtime: Date ext: string isDir: boolean } const downloadsDirDefault = home('Downloads') let downloadsDir = downloadsDirDefault if (!(await pathExists(downloadsDirDefault))) { downloadsDir = await path({ placeholder: 'Select your Downloads folder', onlyDirs: true, hint: 'Could not find ~/Downloads. Pick a folder to scan.', }) } const exts = { images: ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.heic', '.tiff', '.bmp', '.ico'], videos: ['.mp4', '.mov', '.mkv', '.avi', '.webm', '.m4v'], audio: ['.mp3', '.wav', '.m4a', '.flac', '.aac', '.ogg'], archives: ['.zip', '.gz', '.tar', '.tgz', '.rar', '.7z'], documents: [ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.md', '.rtf', '.csv', '.pages', '.key', '.numbers', ], code: [ '.js', '.ts', '.tsx', '.jsx', '.json', '.yml', '.yaml', '.sh', '.py', '.rb', '.go', '.java', '.c', '.cpp', '.cs', '.html', '.css', '.scss', '.swift', '.kt', '.rs', '.sql', ], installers: ['.dmg', '.pkg', '.exe', '.msi'], } const categoryOrder = [ 'Folders', 'Images', 'Videos', 'Audio', 'Documents', 'Archives', 'Code', 'Installers', 'Others', ] const getCategory = (ext: string, isDir: boolean): string => { if (isDir) return 'Folders' if (exts.images.includes(ext)) return 'Images' if (exts.videos.includes(ext)) return 'Videos' if (exts.audio.includes(ext)) return 'Audio' if (exts.documents.includes(ext)) return 'Documents' if (exts.archives.includes(ext)) return 'Archives' if (exts.code.includes(ext)) return 'Code' if (exts.installers.includes(ext)) return 'Installers' return 'Others' } const formatBytes = (bytes: number): string => { if (bytes === 0) return '0 B' const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(1024)) return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${sizes[i]}` } const mdEscape = (s: string): string => s.replace(/\|/g, '\\|').replace(/`/g, '\\`').replace(/\*/g, '\\*').replace(/_/g, '\\_') const entries: Entry[] = [] const names = await readdir(downloadsDir) for await (const name of names) { if (name.startsWith('.')) continue // skip hidden items const filePath = path.join(downloadsDir, name) try { const s = await lstat(filePath) const isDir = s.isDirectory() const ext = isDir ? '' : path.extname(name).toLowerCase() entries.push({ name, filePath, size: isDir ? 0 : s.size, mtime: s.mtime, ext, isDir, }) } catch { // ignore unreadable entries } } const grouped = new Map<string, Entry[]>() let totalFiles = 0 let totalDirs = 0 let totalBytes = 0 for (const e of entries) { const cat = getCategory(e.ext, e.isDir) if (!grouped.has(cat)) grouped.set(cat, []) grouped.get(cat)!.push(e) if (e.isDir) totalDirs++ else { totalFiles++ totalBytes += e.size } } // Sort each group by modified date desc for (const [, list] of grouped) { list.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()) } const dateStr = formatDate(new Date(), 'yyyy-MM-dd HH:mm') const outName = `Downloads-Index-${formatDate(new Date(), 'yyyy-MM-dd')}.md` const outPath = path.join(downloadsDir, outName) const lines: string[] = [] lines.push(`# Downloads Index`) lines.push('') lines.push(`- Generated: ${dateStr}`) lines.push(`- Directory: ${downloadsDir}`) lines.push(`- Items: ${entries.length} (Files: ${totalFiles}, Folders: ${totalDirs})`) lines.push(`- Total File Size: ${formatBytes(totalBytes)}`) lines.push('') for (const cat of categoryOrder) { const list = grouped.get(cat) if (!list || list.length === 0) continue lines.push(`## ${cat} (${list.length})`) lines.push('') lines.push(`| Name | Size | Modified | Path |`) lines.push(`| --- | ---:| --- | --- |`) for (const e of list) { const name = mdEscape(e.name) const size = e.isDir ? '—' : formatBytes(e.size) const modified = formatDate(e.mtime, 'yyyy-MM-dd HH:mm') const link = encodeURI(`file://${e.filePath}`) lines.push(`| ${name} | ${size} | ${modified} | [Open](${link}) |`) } lines.push('') } await writeFile(outPath, lines.join('\n')) await notify({ title: 'Downloads Index Created', body: outPath }) await revealFile(outPath)