Monorepo for Aesthetic.Computer
aesthetic.computer
1// plugins.mjs, 26.02.12
2// Manage Max for Live plugin versions and downloads
3// Collection: aesthetic.plugins
4//
5// Code Prefix:
6// & - M4L Plugins (e.g., &np123a for notepat v1.2.3)
7
8// Schema:
9// {
10// _id: ObjectId,
11// code: string, // Unique short code (6 chars, e.g., "np123a")
12// device: {
13// name: string, // "notepat", "pedal", "metronome"
14// displayName: string, // "AC 🟪 Notepat"
15// category: "instrument" | "effect" | "midi",
16// icon: string, // Emoji or icon identifier
17// },
18// version: {
19// major: number,
20// minor: number,
21// patch: number,
22// string: string, // "1.2.3"
23// },
24// m4l: {
25// downloadUrl: string, // "https://assets.aesthetic.computer/m4l/..."
26// fileName: string, // "AC 🟪 notepat (aesthetic.computer).amxd"
27// fileSize: number, // Bytes
28// checksum: string, // SHA256
29// },
30// metadata: {
31// description: string,
32// piece: string, // AC piece name (e.g., "notepat")
33// width: number,
34// height: number,
35// releaseNotes: string,
36// },
37// stats: {
38// downloads: number,
39// views: number,
40// },
41// createdAt: Date,
42// updatedAt: Date,
43// publishedAt: Date,
44// deprecated: boolean,
45// }
46
47// Generate a short unique code for plugins
48function generateCode(deviceName, version, length = 6) {
49 const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
50 // Use first 2 chars of device name + version numbers + random chars
51 const prefix = deviceName.substring(0, 2);
52 const versionStr = version.string.replace(/\./g, '');
53 let code = prefix + versionStr;
54
55 // Pad with random chars if needed
56 while (code.length < length) {
57 code += chars[Math.floor(Math.random() * chars.length)];
58 }
59
60 return code.substring(0, length);
61}
62
63/**
64 * Create a new plugin record in the database
65 * @param {Object} db - Database connection
66 * @param {Object} pluginData - Plugin data to insert
67 * @returns {Object} Created plugin document
68 */
69export async function createPlugin(db, pluginData) {
70 const plugins = db.collection('plugins');
71
72 // Generate unique code if not provided
73 if (!pluginData.code) {
74 pluginData.code = generateCode(
75 pluginData.device.name,
76 pluginData.version
77 );
78
79 // Ensure uniqueness
80 let exists = await plugins.findOne({ code: pluginData.code });
81 while (exists) {
82 pluginData.code = generateCode(
83 pluginData.device.name,
84 pluginData.version
85 );
86 exists = await plugins.findOne({ code: pluginData.code });
87 }
88 }
89
90 // Set timestamps
91 const now = new Date();
92 pluginData.createdAt = now;
93 pluginData.updatedAt = now;
94
95 // Initialize stats if not provided
96 if (!pluginData.stats) {
97 pluginData.stats = { downloads: 0, views: 0 };
98 }
99
100 // Insert into database
101 const result = await plugins.insertOne(pluginData);
102 pluginData._id = result.insertedId;
103
104 console.log(`✅ Created plugin: ${pluginData.device.displayName} v${pluginData.version.string} (${pluginData.code})`);
105
106 return pluginData;
107}
108
109/**
110 * Get the latest version of each plugin
111 * @param {Object} db - Database connection
112 * @returns {Array} Array of latest plugin versions
113 */
114export async function getLatestPlugins(db) {
115 const plugins = db.collection('plugins');
116
117 // Aggregation pipeline to get latest version of each device
118 const pipeline = [
119 // Filter out deprecated plugins
120 { $match: { deprecated: { $ne: true } } },
121
122 // Sort by version descending
123 { $sort: {
124 "device.name": 1,
125 "version.major": -1,
126 "version.minor": -1,
127 "version.patch": -1
128 }},
129
130 // Group by device name and take first (highest version)
131 {
132 $group: {
133 _id: "$device.name",
134 latestPlugin: { $first: "$$ROOT" }
135 }
136 },
137
138 // Replace root with the plugin document
139 { $replaceRoot: { newRoot: "$latestPlugin" } },
140
141 // Sort by device name for consistent ordering
142 { $sort: { "device.name": 1 } },
143
144 // Project only needed fields (exclude MongoDB _id)
145 {
146 $project: {
147 _id: 0,
148 code: 1,
149 device: 1,
150 version: 1,
151 m4l: 1,
152 metadata: 1,
153 stats: 1,
154 publishedAt: 1
155 }
156 }
157 ];
158
159 const result = await plugins.aggregate(pipeline).toArray();
160 return result;
161}
162
163/**
164 * Get a specific plugin by code
165 * @param {Object} db - Database connection
166 * @param {string} code - Plugin code
167 * @returns {Object|null} Plugin document or null
168 */
169export async function getPluginByCode(db, code) {
170 const plugins = db.collection('plugins');
171 return await plugins.findOne({ code }, { projection: { _id: 0 } });
172}
173
174/**
175 * Get all versions of a specific device
176 * @param {Object} db - Database connection
177 * @param {string} deviceName - Device name (e.g., "notepat")
178 * @returns {Array} Array of all versions sorted by version descending
179 */
180export async function getAllVersions(db, deviceName) {
181 const plugins = db.collection('plugins');
182
183 const versions = await plugins.find(
184 { "device.name": deviceName },
185 { projection: { _id: 0 } }
186 )
187 .sort({
188 "version.major": -1,
189 "version.minor": -1,
190 "version.patch": -1
191 })
192 .toArray();
193
194 return versions;
195}
196
197/**
198 * Increment download counter for a plugin
199 * @param {Object} db - Database connection
200 * @param {string} code - Plugin code
201 * @returns {Object} Update result
202 */
203export async function incrementDownloads(db, code) {
204 const plugins = db.collection('plugins');
205
206 const result = await plugins.updateOne(
207 { code },
208 {
209 $inc: { "stats.downloads": 1 },
210 $set: { updatedAt: new Date() }
211 }
212 );
213
214 return result;
215}
216
217/**
218 * Increment view counter for a plugin
219 * @param {Object} db - Database connection
220 * @param {string} code - Plugin code
221 * @returns {Object} Update result
222 */
223export async function incrementViews(db, code) {
224 const plugins = db.collection('plugins');
225
226 const result = await plugins.updateOne(
227 { code },
228 {
229 $inc: { "stats.views": 1 },
230 $set: { updatedAt: new Date() }
231 }
232 );
233
234 return result;
235}
236
237/**
238 * Mark a plugin as deprecated
239 * @param {Object} db - Database connection
240 * @param {string} code - Plugin code
241 * @returns {Object} Update result
242 */
243export async function deprecatePlugin(db, code) {
244 const plugins = db.collection('plugins');
245
246 const result = await plugins.updateOne(
247 { code },
248 {
249 $set: {
250 deprecated: true,
251 updatedAt: new Date()
252 }
253 }
254 );
255
256 return result;
257}
258
259/**
260 * Update plugin metadata
261 * @param {Object} db - Database connection
262 * @param {string} code - Plugin code
263 * @param {Object} updates - Fields to update
264 * @returns {Object} Update result
265 */
266export async function updatePlugin(db, code, updates) {
267 const plugins = db.collection('plugins');
268
269 const result = await plugins.updateOne(
270 { code },
271 {
272 $set: {
273 ...updates,
274 updatedAt: new Date()
275 }
276 }
277 );
278
279 return result;
280}
281
282/**
283 * Search plugins by category
284 * @param {Object} db - Database connection
285 * @param {string} category - Plugin category ("instrument", "effect", "midi")
286 * @returns {Array} Array of matching plugins
287 */
288export async function getPluginsByCategory(db, category) {
289 const plugins = db.collection('plugins');
290
291 // Get latest version of each device in category
292 const pipeline = [
293 { $match: {
294 "device.category": category,
295 deprecated: { $ne: true }
296 }},
297 { $sort: {
298 "device.name": 1,
299 "version.major": -1,
300 "version.minor": -1,
301 "version.patch": -1
302 }},
303 {
304 $group: {
305 _id: "$device.name",
306 latestPlugin: { $first: "$$ROOT" }
307 }
308 },
309 { $replaceRoot: { newRoot: "$latestPlugin" } },
310 { $sort: { "device.name": 1 } },
311 { $project: { _id: 0 } }
312 ];
313
314 return await plugins.aggregate(pipeline).toArray();
315}
316
317/**
318 * Create indexes for the plugins collection
319 * This should be run once during setup
320 * @param {Object} db - Database connection
321 */
322export async function createIndexes(db) {
323 const plugins = db.collection('plugins');
324
325 await plugins.createIndex({ code: 1 }, { unique: true });
326 await plugins.createIndex({ "device.name": 1 });
327 await plugins.createIndex({ "device.category": 1 });
328 await plugins.createIndex({ publishedAt: -1 });
329 await plugins.createIndex({
330 "version.major": 1,
331 "version.minor": 1,
332 "version.patch": 1
333 });
334 await plugins.createIndex({ updatedAt: -1 });
335 await plugins.createIndex({ deprecated: 1 });
336
337 console.log("✅ Created indexes for plugins collection");
338}
339
340export { generateCode };