tfrere HF Staff Cursor commited on
Commit
faacae5
·
1 Parent(s): 4c19a89

fix: deduplicate forked spaces with identical repo names

Browse files

Forks 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>

Files changed (1) hide show
  1. 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
- allApps.sort((a, b) => {
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 allApps;
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;