Published your App

Replit-Commit-Author: Deployment
Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2b7650db-b0d4-4415-b9bb-cc11e719c2b7
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/ncMMRQ9
Replit-Commit-Deployment-Build-Id: 55db64a6-94a6-4850-b7bf-6b5712a636c3
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic 2026-03-06 10:31:27 +00:00
parent 8cbb08f182
commit a23ae0bdb7
5 changed files with 420 additions and 1204 deletions

View File

@ -0,0 +1,10 @@
{
"DSC06227.jpg": "folx-gallery/DSC06227",
"Ulli Bastian (2).jpg": "folx-gallery/Ulli_Bastian__2_",
"DSC08185.jpg": "folx-gallery/DSC08185",
"DSC08429.jpg": "folx-gallery/DSC08429",
"Julia Buchner 2.jpg": "folx-gallery/Julia_Buchner_2",
"Julia Buchner 1 .jpg": "folx-gallery/Julia_Buchner_1_",
"John Prisco 1.jpg": "folx-gallery/John_Prisco_1",
"Mosaik 4.jpg": "folx-gallery/Mosaik_4"
}

94
server/cloudinary.ts Normal file
View File

@ -0,0 +1,94 @@
import { v2 as cloudinary } from "cloudinary";
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";
export interface CloudinaryGalleryImage {
publicId: string;
fileName: string;
artist: string;
thumb: string;
large: string;
mobile: string;
full: string;
}
function buildUrl(publicId: string, transforms: string): string {
return cloudinary.url(publicId, {
transformation: transforms,
secure: true,
});
}
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"),
};
}
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 result = await cloudinary.uploader.upload(imageUrl, {
public_id: publicId,
overwrite: false,
resource_type: "image",
});
return result.public_id;
} 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 };

View File

@ -1,8 +1,10 @@
import fs from "fs";
import path from "path";
import { uploadToCloudinary, generateImageUrls } from "./cloudinary";
const TOKEN_PATH = path.join(process.cwd(), "server/dropbox-token.json");
const GALLERY_ROOT = "/family room/photos/oddaje/izvajalci selekcija";
const CLOUDINARY_MAP_PATH = path.join(process.cwd(), "server/cloudinary-gallery-map.json");
interface DropboxTokens {
access_token: string;
@ -22,6 +24,23 @@ interface GalleryImage {
artist: string;
}
interface CloudinaryMap {
[fileName: string]: string;
}
function loadCloudinaryMap(): CloudinaryMap {
try {
if (fs.existsSync(CLOUDINARY_MAP_PATH)) {
return JSON.parse(fs.readFileSync(CLOUDINARY_MAP_PATH, "utf-8"));
}
} catch {}
return {};
}
function saveCloudinaryMap(map: CloudinaryMap): void {
fs.writeFileSync(CLOUDINARY_MAP_PATH, JSON.stringify(map, null, 2));
}
function extractArtistFromFileName(fileName: string): string {
if (/^DSC\d/i.test(fileName) || /^IMG[\s_]\d/i.test(fileName)) return "";
const withoutExt = fileName.replace(/\.\w+$/, "");
@ -233,6 +252,28 @@ export async function fetchGalleryFromDropbox(): Promise<GalleryImage[]> {
return galleryCache.data;
}
const cloudinaryMap = loadCloudinaryMap();
const hasCloudinaryImages = Object.keys(cloudinaryMap).length > 0;
if (hasCloudinaryImages) {
const images: GalleryImage[] = [];
for (const [fileName, publicId] of Object.entries(cloudinaryMap)) {
const urls = generateImageUrls(publicId);
images.push({
folder: "Foto All",
fileName,
thumb: urls.thumb,
large: urls.large,
mobile: urls.mobile,
full: urls.full,
artist: extractArtistFromFileName(fileName),
});
}
galleryCache.data = images;
galleryCache.timestamp = Date.now();
return images;
}
const accessToken = await getValidAccessToken();
if (!accessToken) return [];
@ -305,3 +346,55 @@ export async function fetchGalleryFromDropbox(): Promise<GalleryImage[]> {
return [];
}
}
export async function migrateGalleryToCloudinary(): Promise<{ uploaded: number; skipped: number; failed: number }> {
const accessToken = await getValidAccessToken();
if (!accessToken) throw new Error("Dropbox not connected");
const originalEntries = await listFolder(accessToken, ORIGINAL_FOLDER.toLowerCase());
const imgFilter = (e: any) => e[".tag"] === "file" && /\.(jpg|jpeg|png|webp|gif)$/i.test(e.name);
const originalFiles = originalEntries.filter(imgFilter);
const cloudinaryMap = loadCloudinaryMap();
let uploaded = 0;
let skipped = 0;
let failed = 0;
const batchSize = 5;
for (let i = 0; i < originalFiles.length; i += batchSize) {
const batch = originalFiles.slice(i, i + batchSize);
const links = await getTemporaryLinks(accessToken, batch.map((f: any) => f.path_lower));
const uploadPromises = batch.map(async (file: any) => {
const fileName = file.name;
if (cloudinaryMap[fileName]) {
skipped++;
return;
}
const link = links.get(file.path_lower);
if (!link) {
failed++;
return;
}
const publicId = await uploadToCloudinary(link, fileName);
if (publicId) {
cloudinaryMap[fileName] = publicId;
uploaded++;
console.log(`[cloudinary] Uploaded: ${fileName} -> ${publicId}`);
} else {
failed++;
}
});
await Promise.all(uploadPromises);
saveCloudinaryMap(cloudinaryMap);
}
galleryCache.data = [];
galleryCache.timestamp = 0;
return { uploaded, skipped, failed };
}

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ import { seedDatabase } from "./seed";
import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope } from "./horoscope-generator";
import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point";
import { optimizeImage } from "./image-optimizer";
import { getAuthUrl, exchangeCodeForTokens, isConnected, fetchGalleryFromDropbox, getValidAccessToken } from "./dropbox";
import { getAuthUrl, exchangeCodeForTokens, isConnected, fetchGalleryFromDropbox, getValidAccessToken, migrateGalleryToCloudinary } from "./dropbox";
import multer from "multer";
import path from "path";
import fs from "fs";
@ -353,7 +353,16 @@ export async function registerRoutes(
}
});
// Gallery API - serves optimized photos from Dropbox
app.post("/api/gallery/migrate-to-cloudinary", async (_req, res) => {
try {
const result = await migrateGalleryToCloudinary();
res.json(result);
} catch (err: any) {
res.status(500).json({ message: err.message });
}
});
// Gallery API - serves optimized photos from Dropbox/Cloudinary
app.get("/api/gallery", async (req, res) => {
try {
let data: any[] = [];