Created
June 19, 2025 15:36
-
-
Save CPlusPatch/69abad98aaf18bc9721fd820bef33d63 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { join } from 'path'; | |
import { mkdir } from 'fs/promises'; | |
/** | |
* Interface for emoji object from API | |
*/ | |
interface Emoji { | |
aliases?: string[]; | |
name: string; | |
category?: string; | |
url: string; | |
} | |
/** | |
* Extracts file extension from Content-Disposition header or URL | |
* @param header - Content-Disposition header value | |
* @param url - URL as fallback for extension extraction | |
* @returns The extracted file extension (with dot) or .png as default | |
*/ | |
function getFileExtension(header: string | null, url: string): string { | |
// Try getting filename from header first | |
if (header) { | |
// Try filename* format first (UTF-8 encoded) | |
const filenameStarMatch = header.match(/filename\*=UTF-8''([^;]+)/i); | |
if (filenameStarMatch && filenameStarMatch[1]) { | |
const decodedFilename = decodeURIComponent(filenameStarMatch[1]); | |
const extMatch = decodedFilename.match(/(\.[^.]+)$/); | |
if (extMatch && extMatch[1]) { | |
return extMatch[1].toLowerCase(); | |
} | |
} | |
// Then try regular filename format | |
const filenameMatch = header.match(/filename="([^"]+)"/i); | |
if (filenameMatch && filenameMatch[1]) { | |
const filename = filenameMatch[1]; | |
const extMatch = filename.match(/(\.[^.]+)$/); | |
if (extMatch && extMatch[1]) { | |
return extMatch[1].toLowerCase(); | |
} | |
} | |
} | |
// Fall back to URL for extension | |
const urlExtMatch = url.match(/(\.[^./?]+)(?:\?.*)?$/); | |
if (urlExtMatch && urlExtMatch[1]) { | |
return urlExtMatch[1].toLowerCase(); | |
} | |
// Default extension if none found | |
return '.png'; | |
} | |
/** | |
* Downloads a file from a URL and saves it using Bun's fetch API | |
* @param url - URL of the file to download | |
* @param dirPath - Path where the file should be saved | |
* @param name - Emoji name to use as the base filename | |
* @returns The final path where the file was saved | |
*/ | |
async function downloadFile(url: string, dirPath: string, name: string): Promise<string> { | |
try { | |
// Fetch the file with automatic redirect handling | |
const response = await fetch(url); | |
if (!response.ok) { | |
throw new Error(`Failed to download file: ${response.status}`); | |
} | |
// Get file extension from Content-Disposition header or URL | |
const contentDisposition = response.headers.get('content-disposition'); | |
const fileExtension = getFileExtension(contentDisposition, url); | |
// Use the emoji name as the base filename, combined with the detected extension | |
const filename = `${name}${fileExtension}`; | |
const finalPath = join(dirPath, filename); | |
// Get the file data as an ArrayBuffer | |
const fileData = await response.arrayBuffer(); | |
// Write the file using Bun's File API | |
await Bun.write(finalPath, fileData); | |
return finalPath; | |
} catch (error) { | |
throw new Error(`Download failed: ${(error as Error).message}`); | |
} | |
} | |
/** | |
* Creates a directory if it doesn't exist | |
* @param dirPath - Path of the directory to create | |
*/ | |
async function ensureDirectoryExists(dirPath: string): Promise<void> { | |
try { | |
await mkdir(dirPath, { recursive: true }); | |
} catch (error) { | |
// Ignore error if directory already exists | |
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') { | |
throw error; | |
} | |
} | |
} | |
// Constants | |
const API_URL = 'https://lea.pet/api/emojis'; | |
// Main execution with top-level await | |
console.log('Fetching emoji data from API...'); | |
try { | |
// Fetch the emoji data using Bun's fetch API | |
const response = await fetch(API_URL); | |
if (!response.ok) { | |
throw new Error(`API request failed with status code ${response.status}`); | |
} | |
const emojisJson = (await response.json()).emojis as Emoji[]; | |
// Check if the response has the expected structure | |
if (!Array.isArray(emojisJson)) { | |
throw new Error('Invalid API response format: expected an array'); | |
} | |
// Group emojis by category | |
const emojisByCategory: Record<string, Emoji[]> = {}; | |
for (const emoji of emojisJson) { | |
// Skip if no URL is provided | |
if (!emoji.url) continue; | |
// Use 'default' if no category is specified | |
const category = emoji.category || 'default'; | |
if (!emojisByCategory[category]) { | |
emojisByCategory[category] = []; | |
} | |
emojisByCategory[category].push(emoji); | |
} | |
// Create category directories and download emojis | |
console.log(`Found ${Object.keys(emojisByCategory).length} categories`); | |
for (const [category, emojis] of Object.entries(emojisByCategory)) { | |
const categoryDir = join(import.meta.dir, category); | |
await ensureDirectoryExists(categoryDir); | |
console.log(`Downloading ${emojis.length} emojis for category: ${category}`); | |
// Download each emoji in the category | |
await Promise.all(emojis.map(async (emoji) => { | |
try { | |
const savedPath = await downloadFile(emoji.url, categoryDir, emoji.name); | |
const savedFilename = savedPath.split('/').pop() || emoji.name; | |
console.log(`Downloaded: ${savedFilename}`); | |
} catch (error) { | |
console.error(`Failed to download ${emoji.name}: ${(error as Error).message}`); | |
} | |
})); | |
} | |
console.log('Download complete!'); | |
} catch (error) { | |
console.error('An error occurred:', (error as Error).message); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment