Spaces:
Running
Running
File size: 8,088 Bytes
e8700c6 eee87e9 e8700c6 4d77ab0 e8700c6 4d77ab0 e8700c6 4d77ab0 e8700c6 4d77ab0 e8700c6 4d77ab0 e8700c6 4d77ab0 e8700c6 4d77ab0 faacae5 4d77ab0 e8700c6 faacae5 e8700c6 faacae5 e8700c6 4d77ab0 e8700c6 faacae5 e8700c6 45671b3 e8700c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.PORT || 7860;
// Cache configuration
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const OFFICIAL_APP_LIST_URL = 'https://huggingface.co/datasets/pollen-robotics/reachy-mini-official-app-store/raw/main/app-list.json';
const HF_SPACES_API = 'https://huggingface.co/api/spaces';
// Note: HF API doesn't support pagination with filter=, so we use a high limit
const HF_SPACES_LIMIT = 1000;
// In-memory cache
let appsCache = {
data: null,
lastFetch: null,
fetching: false,
};
// Fetch apps from HuggingFace API
// Returns format compatible with desktop app (with url, source_kind, extra)
async function fetchAppsFromHF() {
console.log('[Cache] Fetching apps from HuggingFace API...');
try {
// 1. Fetch official app IDs
const officialResponse = await fetch(OFFICIAL_APP_LIST_URL);
let officialIdList = [];
if (officialResponse.ok) {
officialIdList = await officialResponse.json();
}
const officialSet = new Set(officialIdList.map(id => id.toLowerCase()));
// 2. Fetch all spaces with reachy_mini tag
// Note: HF API doesn't support pagination with filter=, so we use a high limit
const spacesResponse = await fetch(`${HF_SPACES_API}?filter=reachy_mini&full=true&limit=${HF_SPACES_LIMIT}`);
if (!spacesResponse.ok) {
throw new Error(`HF API returned ${spacesResponse.status}`);
}
const allSpaces = await spacesResponse.json();
console.log(`[Cache] Fetched ${allSpaces.length} spaces from HuggingFace`);
// 3. Build apps list in desktop-compatible format
const allApps = allSpaces.map(space => {
const spaceId = space.id || '';
const tags = space.tags || [];
const isOfficial = officialSet.has(spaceId.toLowerCase());
const isPythonApp = tags.includes('reachy_mini_python_app');
const author = spaceId.split('/')[0];
const name = spaceId.split('/').pop();
return {
// Core fields (used by both website and desktop)
id: spaceId,
name,
description: space.cardData?.short_description || '',
url: `https://huggingface.co/spaces/${spaceId}`,
source_kind: 'hf_space',
isOfficial,
// Extra metadata (desktop-compatible structure)
extra: {
id: spaceId,
author,
likes: space.likes || 0,
downloads: space.downloads || 0,
createdAt: space.createdAt || null,
lastModified: space.lastModified,
runtime: space.runtime || null,
tags,
isPythonApp,
cardData: {
emoji: space.cardData?.emoji || (isPythonApp ? '📦' : '🌐'),
short_description: space.cardData?.short_description || '',
sdk: space.cardData?.sdk || null,
tags: space.cardData?.tags || [],
// Preserve other cardData fields
...space.cardData,
},
},
};
});
// Deduplicate by name: forks keep the same repo name (e.g. 4 spaces
// named "reachy_mini_conversation_app" from different authors).
// Priority: 1) official, 2) oldest (original), 3) most likes as tiebreaker.
const deduped = new Map();
for (const app of allApps) {
const key = app.name.toLowerCase();
const existing = deduped.get(key);
if (!existing) {
deduped.set(key, app);
continue;
}
// Official always wins
if (app.isOfficial && !existing.isOfficial) {
deduped.set(key, app);
continue;
}
if (existing.isOfficial) continue;
// Oldest wins (the original is created before its forks)
const appDate = app.extra.createdAt ? new Date(app.extra.createdAt).getTime() : Infinity;
const existingDate = existing.extra.createdAt ? new Date(existing.extra.createdAt).getTime() : Infinity;
if (appDate < existingDate) {
deduped.set(key, app);
} else if (appDate === existingDate && (app.extra.likes || 0) > (existing.extra.likes || 0)) {
deduped.set(key, app);
}
}
const uniqueApps = [...deduped.values()];
console.log(`[Cache] Deduplicated ${allApps.length} → ${uniqueApps.length} apps (removed ${allApps.length - uniqueApps.length} forks with duplicate names)`);
// Sort: official first, then by likes
uniqueApps.sort((a, b) => {
if (a.isOfficial !== b.isOfficial) {
return a.isOfficial ? -1 : 1;
}
return (b.extra.likes || 0) - (a.extra.likes || 0);
});
return uniqueApps;
} catch (err) {
console.error('[Cache] Error fetching apps:', err);
throw err;
}
}
// Get apps with caching
async function getApps() {
const now = Date.now();
// Return cache if valid
if (appsCache.data && appsCache.lastFetch && (now - appsCache.lastFetch) < CACHE_TTL_MS) {
const ageMinutes = Math.round((now - appsCache.lastFetch) / 60000);
console.log(`[Cache] Returning cached data (age: ${ageMinutes} min)`);
return appsCache.data;
}
// Prevent concurrent fetches
if (appsCache.fetching) {
console.log('[Cache] Fetch already in progress, returning stale data');
return appsCache.data || [];
}
appsCache.fetching = true;
try {
const apps = await fetchAppsFromHF();
appsCache.data = apps;
appsCache.lastFetch = now;
console.log(`[Cache] Cache updated with ${apps.length} apps`);
return apps;
} catch (err) {
// On error, return stale cache if available
if (appsCache.data) {
console.log('[Cache] Fetch failed, returning stale cache');
return appsCache.data;
}
throw err;
} finally {
appsCache.fetching = false;
}
}
// API endpoint
app.get('/api/apps', async (req, res) => {
try {
const apps = await getApps();
res.json({
apps,
cached: true,
cacheAge: appsCache.lastFetch ? Math.round((Date.now() - appsCache.lastFetch) / 1000) : 0,
count: apps.length,
});
} catch (err) {
console.error('[API] Error:', err);
res.status(500).json({ error: 'Failed to fetch apps' });
}
});
// OAuth config endpoint - expose public OAuth variables to the frontend
// (Docker Spaces don't auto-inject window.huggingface.variables like static Spaces)
app.get('/api/oauth-config', (req, res) => {
const clientId = process.env.OAUTH_CLIENT_ID;
const scopes = process.env.OAUTH_SCOPES || 'openid profile';
if (!clientId) {
return res.status(503).json({
error: 'OAuth not configured',
hint: 'Make sure hf_oauth: true is set in README.md and the Space has been rebuilt',
});
}
res.json({ clientId, scopes });
});
// Health check
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
cacheStatus: appsCache.data ? 'warm' : 'cold',
cacheAge: appsCache.lastFetch ? Math.round((Date.now() - appsCache.lastFetch) / 1000) : null,
appsCount: appsCache.data?.length || 0,
});
});
// Force cache refresh (for admin use)
app.post('/api/refresh', async (req, res) => {
try {
appsCache.lastFetch = null; // Invalidate cache
const apps = await getApps();
res.json({ success: true, count: apps.length });
} catch (err) {
res.status(500).json({ error: 'Failed to refresh cache' });
}
});
// Serve static files from the dist folder
app.use(express.static(path.join(__dirname, '../dist'), {
maxAge: '1y',
etag: true,
}));
// SPA fallback - serve index.html for all other routes
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html'));
});
// Pre-warm cache on startup
async function warmCache() {
console.log('[Startup] Pre-warming cache...');
try {
await getApps();
console.log('[Startup] Cache warmed successfully');
} catch (err) {
console.error('[Startup] Failed to warm cache:', err);
}
}
// Start server
app.listen(PORT, () => {
console.log(`[Server] Running on port ${PORT}`);
warmCache();
});
|