Spaces:
Running
Running
fix: deduplicate forked spaces with identical repo names
Browse filesForks of apps (e.g. reachy_mini_conversation_app) keep the same repo
name, causing duplicates in the app store. Deduplicate by name with
priority: 1) official, 2) oldest (original), 3) most likes.
Also expose createdAt from HF API for accurate original detection.
Co-authored-by: Cursor <cursoragent@cursor.com>
- server/index.js +33 -2
server/index.js
CHANGED
|
@@ -68,6 +68,7 @@ async function fetchAppsFromHF() {
|
|
| 68 |
author,
|
| 69 |
likes: space.likes || 0,
|
| 70 |
downloads: space.downloads || 0,
|
|
|
|
| 71 |
lastModified: space.lastModified,
|
| 72 |
runtime: space.runtime || null,
|
| 73 |
tags,
|
|
@@ -84,15 +85,45 @@ async function fetchAppsFromHF() {
|
|
| 84 |
};
|
| 85 |
});
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
// Sort: official first, then by likes
|
| 88 |
-
|
| 89 |
if (a.isOfficial !== b.isOfficial) {
|
| 90 |
return a.isOfficial ? -1 : 1;
|
| 91 |
}
|
| 92 |
return (b.extra.likes || 0) - (a.extra.likes || 0);
|
| 93 |
});
|
| 94 |
|
| 95 |
-
return
|
| 96 |
} catch (err) {
|
| 97 |
console.error('[Cache] Error fetching apps:', err);
|
| 98 |
throw err;
|
|
|
|
| 68 |
author,
|
| 69 |
likes: space.likes || 0,
|
| 70 |
downloads: space.downloads || 0,
|
| 71 |
+
createdAt: space.createdAt || null,
|
| 72 |
lastModified: space.lastModified,
|
| 73 |
runtime: space.runtime || null,
|
| 74 |
tags,
|
|
|
|
| 85 |
};
|
| 86 |
});
|
| 87 |
|
| 88 |
+
// Deduplicate by name: forks keep the same repo name (e.g. 4 spaces
|
| 89 |
+
// named "reachy_mini_conversation_app" from different authors).
|
| 90 |
+
// Priority: 1) official, 2) oldest (original), 3) most likes as tiebreaker.
|
| 91 |
+
const deduped = new Map();
|
| 92 |
+
for (const app of allApps) {
|
| 93 |
+
const key = app.name.toLowerCase();
|
| 94 |
+
const existing = deduped.get(key);
|
| 95 |
+
if (!existing) {
|
| 96 |
+
deduped.set(key, app);
|
| 97 |
+
continue;
|
| 98 |
+
}
|
| 99 |
+
// Official always wins
|
| 100 |
+
if (app.isOfficial && !existing.isOfficial) {
|
| 101 |
+
deduped.set(key, app);
|
| 102 |
+
continue;
|
| 103 |
+
}
|
| 104 |
+
if (existing.isOfficial) continue;
|
| 105 |
+
// Oldest wins (the original is created before its forks)
|
| 106 |
+
const appDate = app.extra.createdAt ? new Date(app.extra.createdAt).getTime() : Infinity;
|
| 107 |
+
const existingDate = existing.extra.createdAt ? new Date(existing.extra.createdAt).getTime() : Infinity;
|
| 108 |
+
if (appDate < existingDate) {
|
| 109 |
+
deduped.set(key, app);
|
| 110 |
+
} else if (appDate === existingDate && (app.extra.likes || 0) > (existing.extra.likes || 0)) {
|
| 111 |
+
deduped.set(key, app);
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
const uniqueApps = [...deduped.values()];
|
| 115 |
+
|
| 116 |
+
console.log(`[Cache] Deduplicated ${allApps.length} → ${uniqueApps.length} apps (removed ${allApps.length - uniqueApps.length} forks with duplicate names)`);
|
| 117 |
+
|
| 118 |
// Sort: official first, then by likes
|
| 119 |
+
uniqueApps.sort((a, b) => {
|
| 120 |
if (a.isOfficial !== b.isOfficial) {
|
| 121 |
return a.isOfficial ? -1 : 1;
|
| 122 |
}
|
| 123 |
return (b.extra.likes || 0) - (a.extra.likes || 0);
|
| 124 |
});
|
| 125 |
|
| 126 |
+
return uniqueApps;
|
| 127 |
} catch (err) {
|
| 128 |
console.error('[Cache] Error fetching apps:', err);
|
| 129 |
throw err;
|