folx-tv/server/dropbox.ts
sebastjanartic 39d43cd876 Improve gallery loading speed with image optimization and lazy loading
Implement a thumbnail proxy endpoint and lazy loading for gallery images to significantly reduce page load times and improve user experience.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1488855f-6772-48e1-9244-282d1cc91352
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/VgutZ7W
Replit-Helium-Checkpoint-Created: true
2026-03-01 18:52:46 +00:00

262 lines
7.1 KiB
TypeScript

import fs from "fs";
import path from "path";
const TOKEN_PATH = path.join(process.cwd(), "server/dropbox-token.json");
const GALLERY_ROOT = "/family room/photos/oddaje/izvajalci selekcija";
interface DropboxTokens {
access_token: string;
refresh_token: string;
expires_at: number;
app_key?: string;
app_secret?: string;
}
interface GalleryImage {
folder: string;
fileName: string;
thumb: string;
large: string;
full: string;
}
function getAppKey(): string {
const tokens = loadTokens();
return tokens?.app_key || process.env.DROPBOX_APP_KEY || "";
}
function getAppSecret(): string {
const tokens = loadTokens();
return tokens?.app_secret || process.env.DROPBOX_APP_SECRET || "";
}
export function getAuthUrl(redirectUri: string): string {
const params = new URLSearchParams({
client_id: getAppKey(),
response_type: "code",
redirect_uri: redirectUri,
token_access_type: "offline",
});
return `https://www.dropbox.com/oauth2/authorize?${params.toString()}`;
}
export async function exchangeCodeForTokens(code: string, redirectUri: string): Promise<DropboxTokens> {
const appKey = getAppKey();
const appSecret = getAppSecret();
const resp = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
code,
grant_type: "authorization_code",
redirect_uri: redirectUri,
client_id: appKey,
client_secret: appSecret,
}),
});
if (!resp.ok) {
const err = await resp.text();
throw new Error(`Token exchange failed: ${err}`);
}
const data = await resp.json();
const tokens: DropboxTokens = {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + data.expires_in * 1000,
app_key: appKey,
app_secret: appSecret,
};
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
return tokens;
}
function loadTokens(): DropboxTokens | null {
try {
if (fs.existsSync(TOKEN_PATH)) {
return JSON.parse(fs.readFileSync(TOKEN_PATH, "utf-8"));
}
} catch {}
return null;
}
async function refreshAccessToken(refreshToken: string): Promise<DropboxTokens> {
const appKey = getAppKey();
const appSecret = getAppSecret();
const resp = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: appKey,
client_secret: appSecret,
}),
});
if (!resp.ok) {
const err = await resp.text();
throw new Error(`Token refresh failed: ${err}`);
}
const data = await resp.json();
const tokens: DropboxTokens = {
access_token: data.access_token,
refresh_token: refreshToken,
expires_at: Date.now() + data.expires_in * 1000,
app_key: appKey,
app_secret: appSecret,
};
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
return tokens;
}
export async function getValidAccessToken(): Promise<string | null> {
let tokens = loadTokens();
if (!tokens) return null;
if (Date.now() > tokens.expires_at - 60000) {
try {
tokens = await refreshAccessToken(tokens.refresh_token);
} catch (err: any) {
console.error("Token refresh failed:", err.message);
return null;
}
}
return tokens.access_token;
}
export function isConnected(): boolean {
return loadTokens() !== null;
}
async function listFolder(accessToken: string, folderPath: string, recursive = false): Promise<any[]> {
const entries: any[] = [];
let cursor: string | null = null;
let hasMore = true;
while (hasMore) {
const url = cursor
? "https://api.dropboxapi.com/2/files/list_folder/continue"
: "https://api.dropboxapi.com/2/files/list_folder";
const body = cursor
? { cursor }
: { path: folderPath, recursive, limit: 2000 };
const resp = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!resp.ok) {
const err = await resp.text();
throw new Error(`List folder failed: ${err}`);
}
const data = await resp.json();
entries.push(...data.entries);
hasMore = data.has_more;
cursor = data.cursor;
}
return entries;
}
async function getTemporaryLinks(accessToken: string, paths: string[]): Promise<Map<string, string>> {
const results = new Map<string, string>();
const batchSize = 10;
for (let i = 0; i < paths.length; i += batchSize) {
const batch = paths.slice(i, i + batchSize);
const promises = batch.map(async (p) => {
try {
const resp = await fetch("https://api.dropboxapi.com/2/files/get_temporary_link", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ path: p }),
});
if (resp.ok) {
const data = await resp.json();
results.set(p.toLowerCase(), data.link);
}
} catch {}
});
await Promise.all(promises);
}
return results;
}
const galleryCache: { data: GalleryImage[]; timestamp: number } = { data: [], timestamp: 0 };
const CACHE_DURATION = 30 * 60 * 1000;
export async function fetchGalleryFromDropbox(): Promise<GalleryImage[]> {
if (galleryCache.data.length > 0 && Date.now() - galleryCache.timestamp < CACHE_DURATION) {
return galleryCache.data;
}
const accessToken = await getValidAccessToken();
if (!accessToken) return [];
try {
const topEntries = await listFolder(accessToken, GALLERY_ROOT);
const folders = topEntries.filter((e: any) => e[".tag"] === "folder");
const images: GalleryImage[] = [];
for (const folder of folders) {
try {
const folderName = folder.name;
const folderEntries = await listFolder(accessToken, folder.path_lower);
const imageFiles = folderEntries.filter(
(e: any) => e[".tag"] === "file" && /\.(jpg|jpeg|png|webp|gif)$/i.test(e.name)
);
const paths = imageFiles.map((f: any) => f.path_lower);
const tempLinks = await getTemporaryLinks(accessToken, paths);
for (const file of imageFiles) {
const link = tempLinks.get(file.path_lower) || "";
if (link) {
images.push({
folder: folderName,
fileName: file.name,
thumb: link,
large: link,
full: link,
});
}
}
} catch (err: any) {
console.error(`Folder ${folder.name} error:`, err.message);
}
}
if (images.length > 0) {
galleryCache.data = images;
galleryCache.timestamp = Date.now();
const galleryPath = path.join(process.cwd(), "server/gallery-data.json");
fs.writeFileSync(galleryPath, JSON.stringify(images, null, 2));
}
return images;
} catch (err: any) {
console.error("Dropbox gallery fetch error:", err.message);
return [];
}
}