folx-tv/server/cloudinary.ts
sebastjanartic b8c01627dc Complete the migration of all gallery images to Cloudinary
Implement image compression using Sharp to handle uploads exceeding 10MB, update Cloudinary URL generation, and finalize the gallery migration process.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ecf3cabf-0fca-4959-8ab7-e18c7b222cf7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/ncMMRQ9
Replit-Helium-Checkpoint-Created: true
2026-03-06 10:38:59 +00:00

130 lines
3.7 KiB
TypeScript

import { v2 as cloudinary } from "cloudinary";
import sharp from "sharp";
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
const GALLERY_FOLDER = "folx-gallery";
const MAX_UPLOAD_SIZE = 9 * 1024 * 1024;
export interface CloudinaryGalleryImage {
publicId: string;
fileName: string;
artist: string;
thumb: string;
large: string;
mobile: string;
full: string;
}
function buildUrl(publicId: string, transforms: string): string {
const cloudName = process.env.CLOUDINARY_CLOUD_NAME || "djqxt0pf3";
return `https://res.cloudinary.com/${cloudName}/image/upload/${transforms}/${publicId}`;
}
export function generateImageUrls(publicId: string): { thumb: string; large: string; mobile: string; full: string } {
return {
thumb: buildUrl(publicId, "c_fill,g_face:center,w_300,h_300,q_auto,f_auto"),
mobile: buildUrl(publicId, "c_fill,g_auto,w_600,h_338,q_auto,f_auto"),
large: buildUrl(publicId, "c_fit,w_1200,q_auto,f_auto"),
full: buildUrl(publicId, "c_fit,w_1800,q_auto,f_auto"),
};
}
async function downloadAndCompress(imageUrl: string): Promise<Buffer> {
const resp = await fetch(imageUrl);
if (!resp.ok) throw new Error(`Download failed: ${resp.status}`);
const arrayBuffer = await resp.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
if (buffer.length <= MAX_UPLOAD_SIZE) {
return buffer;
}
const compressed = await sharp(buffer)
.resize({ width: 4000, withoutEnlargement: true })
.jpeg({ quality: 85 })
.toBuffer();
if (compressed.length > MAX_UPLOAD_SIZE) {
return sharp(buffer)
.resize({ width: 3000, withoutEnlargement: true })
.jpeg({ quality: 75 })
.toBuffer();
}
return compressed;
}
export async function uploadToCloudinary(imageUrl: string, fileName: string): Promise<string | null> {
const publicId = `${GALLERY_FOLDER}/${fileName.replace(/\.\w+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_")}`;
try {
const existing = await cloudinary.api.resource(publicId).catch(() => null);
if (existing) {
return publicId;
}
} catch {}
try {
const buffer = await downloadAndCompress(imageUrl);
return new Promise((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream(
{
public_id: publicId,
overwrite: false,
resource_type: "image",
},
(error, result) => {
if (error) reject(error);
else resolve(result!.public_id);
}
);
uploadStream.end(buffer);
});
} catch (err: any) {
console.error(`Cloudinary upload failed for ${fileName}:`, err.message);
return null;
}
}
export async function checkCloudinaryImage(publicId: string): Promise<boolean> {
try {
await cloudinary.api.resource(publicId);
return true;
} catch {
return false;
}
}
export async function listCloudinaryGallery(): Promise<string[]> {
try {
const result = await cloudinary.api.resources({
type: "upload",
prefix: GALLERY_FOLDER + "/",
max_results: 500,
resource_type: "image",
});
return result.resources.map((r: any) => r.public_id);
} catch (err: any) {
console.error("Cloudinary list failed:", err.message);
return [];
}
}
export function extractArtistFromPublicId(publicId: string): string {
const name = publicId.replace(`${GALLERY_FOLDER}/`, "").replace(/_/g, " ");
if (/^DSC\d/i.test(name) || /^IMG[\s_]\d/i.test(name)) return "";
const cleaned = name
.replace(/\s*\(\d+\)\s*$/, "")
.replace(/\s*\d+\s*$/, "")
.replace(/\s*\(\d+\)\s*$/, "");
return cleaned.trim();
}
export { cloudinary };