Skip to content

Instantly share code, notes, and snippets.

@CPlusPatch
Created June 19, 2025 15:36
Show Gist options
  • Save CPlusPatch/69abad98aaf18bc9721fd820bef33d63 to your computer and use it in GitHub Desktop.
Save CPlusPatch/69abad98aaf18bc9721fd820bef33d63 to your computer and use it in GitHub Desktop.
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