tunecamp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/.env.local +2 -0
  2. package/.vercel/README.txt +11 -0
  3. package/.vercel/project.json +1 -0
  4. package/LICENSE +22 -0
  5. package/README.md +554 -0
  6. package/dist/cli.d.ts +6 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +172 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/generator/embedGenerator.d.ts +38 -0
  11. package/dist/generator/embedGenerator.d.ts.map +1 -0
  12. package/dist/generator/embedGenerator.js +92 -0
  13. package/dist/generator/embedGenerator.js.map +1 -0
  14. package/dist/generator/feedGenerator.d.ts +50 -0
  15. package/dist/generator/feedGenerator.d.ts.map +1 -0
  16. package/dist/generator/feedGenerator.js +167 -0
  17. package/dist/generator/feedGenerator.js.map +1 -0
  18. package/dist/generator/podcastFeedGenerator.d.ts +54 -0
  19. package/dist/generator/podcastFeedGenerator.d.ts.map +1 -0
  20. package/dist/generator/podcastFeedGenerator.js +173 -0
  21. package/dist/generator/podcastFeedGenerator.js.map +1 -0
  22. package/dist/generator/proceduralCoverGenerator.d.ts +51 -0
  23. package/dist/generator/proceduralCoverGenerator.d.ts.map +1 -0
  24. package/dist/generator/proceduralCoverGenerator.js +228 -0
  25. package/dist/generator/proceduralCoverGenerator.js.map +1 -0
  26. package/dist/generator/siteGenerator.d.ts +55 -0
  27. package/dist/generator/siteGenerator.d.ts.map +1 -0
  28. package/dist/generator/siteGenerator.js +539 -0
  29. package/dist/generator/siteGenerator.js.map +1 -0
  30. package/dist/generator/templateEngine.d.ts +13 -0
  31. package/dist/generator/templateEngine.d.ts.map +1 -0
  32. package/dist/generator/templateEngine.js +146 -0
  33. package/dist/generator/templateEngine.js.map +1 -0
  34. package/dist/index.d.ts +12 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +32 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/parser/catalogParser.d.ts +13 -0
  39. package/dist/parser/catalogParser.d.ts.map +1 -0
  40. package/dist/parser/catalogParser.js +120 -0
  41. package/dist/parser/catalogParser.js.map +1 -0
  42. package/dist/tools/generate-codes.d.ts +14 -0
  43. package/dist/tools/generate-codes.d.ts.map +1 -0
  44. package/dist/tools/generate-codes.js +274 -0
  45. package/dist/tools/generate-codes.js.map +1 -0
  46. package/dist/tools/generate-sea-pair.d.ts +14 -0
  47. package/dist/tools/generate-sea-pair.d.ts.map +1 -0
  48. package/dist/tools/generate-sea-pair.js +111 -0
  49. package/dist/tools/generate-sea-pair.js.map +1 -0
  50. package/dist/types/index.d.ts +117 -0
  51. package/dist/types/index.d.ts.map +1 -0
  52. package/dist/types/index.js +5 -0
  53. package/dist/types/index.js.map +1 -0
  54. package/dist/utils/audioUtils.d.ts +9 -0
  55. package/dist/utils/audioUtils.d.ts.map +1 -0
  56. package/dist/utils/audioUtils.js +67 -0
  57. package/dist/utils/audioUtils.js.map +1 -0
  58. package/dist/utils/configUtils.d.ts +11 -0
  59. package/dist/utils/configUtils.d.ts.map +1 -0
  60. package/dist/utils/configUtils.js +50 -0
  61. package/dist/utils/configUtils.js.map +1 -0
  62. package/dist/utils/fileUtils.d.ts +14 -0
  63. package/dist/utils/fileUtils.d.ts.map +1 -0
  64. package/dist/utils/fileUtils.js +73 -0
  65. package/dist/utils/fileUtils.js.map +1 -0
  66. package/examples/artist-free/README.md +36 -0
  67. package/examples/artist-paycurtain/README.md +49 -0
  68. package/examples/label/README.md +33 -0
  69. package/gundb-keypair.json +8 -0
  70. package/logo.svg +30 -0
  71. package/package-lock.json +1176 -0
  72. package/package.json +42 -0
  73. package/public/assets/community-registry.js +291 -0
  74. package/public/assets/download-stats.js +263 -0
  75. package/public/assets/player.js +219 -0
  76. package/public/assets/style.css +1170 -0
  77. package/public/assets/theme-widget.js +353 -0
  78. package/public/assets/unlock-codes.js +225 -0
  79. package/public/atom.xml +22 -0
  80. package/public/catalog.m3u +3 -0
  81. package/public/feed.xml +22 -0
  82. package/public/image.png +0 -0
  83. package/public/index.html +249 -0
  84. package/public/logo.svg +30 -0
  85. package/public/releases/chirichetto/Homologo - Chirichetto.wav +0 -0
  86. package/public/releases/chirichetto/cover.png +0 -0
  87. package/public/releases/chirichetto/embed-code.txt +16 -0
  88. package/public/releases/chirichetto/embed-compact.txt +8 -0
  89. package/public/releases/chirichetto/embed.html +39 -0
  90. package/public/releases/chirichetto/index.html +389 -0
  91. package/public/releases/chirichetto/playlist.m3u +3 -0
  92. package/templates/dark/assets/community-registry.js +291 -0
  93. package/templates/dark/assets/download-stats.js +263 -0
  94. package/templates/dark/assets/player.js +219 -0
  95. package/templates/dark/assets/style.css +740 -0
  96. package/templates/dark/index.hbs +73 -0
  97. package/templates/dark/layout.hbs +84 -0
  98. package/templates/dark/release.hbs +212 -0
  99. package/templates/default/assets/community-registry.js +291 -0
  100. package/templates/default/assets/download-stats.js +263 -0
  101. package/templates/default/assets/player.js +219 -0
  102. package/templates/default/assets/style.css +1170 -0
  103. package/templates/default/assets/theme-widget.js +353 -0
  104. package/templates/default/assets/unlock-codes.js +225 -0
  105. package/templates/default/index.hbs +188 -0
  106. package/templates/default/layout.hbs +117 -0
  107. package/templates/default/release.hbs +553 -0
  108. package/templates/minimal/assets/community-registry.js +291 -0
  109. package/templates/minimal/assets/download-stats.js +263 -0
  110. package/templates/minimal/assets/player.js +219 -0
  111. package/templates/minimal/assets/style.css +796 -0
  112. package/templates/minimal/index.hbs +73 -0
  113. package/templates/minimal/layout.hbs +84 -0
  114. package/templates/minimal/release.hbs +212 -0
  115. package/templates/retro/assets/community-registry.js +291 -0
  116. package/templates/retro/assets/download-stats.js +263 -0
  117. package/templates/retro/assets/player.js +219 -0
  118. package/templates/retro/assets/style.css +872 -0
  119. package/templates/retro/index.hbs +73 -0
  120. package/templates/retro/layout.hbs +84 -0
  121. package/templates/retro/release.hbs +212 -0
  122. package/templates/translucent/assets/community-registry.js +291 -0
  123. package/templates/translucent/assets/download-stats.js +263 -0
  124. package/templates/translucent/assets/player.js +219 -0
  125. package/templates/translucent/assets/style.css +1352 -0
  126. package/templates/translucent/index.hbs +73 -0
  127. package/templates/translucent/layout.hbs +84 -0
  128. package/templates/translucent/release.hbs +212 -0
  129. package/website/community.html +492 -0
  130. package/website/index.html +195 -0
  131. package/website/styles.css +396 -0
  132. package/website/tunecamp.svg +30 -0
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Interactive Theme Widget
3
+ * Allows users to customize theme colors in real-time
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ // Theme Widget Class
10
+ class ThemeWidget {
11
+ constructor() {
12
+ this.isOpen = false;
13
+ this.settings = this.loadSettings();
14
+ this.createWidget();
15
+ this.applySettings();
16
+ }
17
+
18
+ // Default theme settings
19
+ getDefaults() {
20
+ return {
21
+ primaryColor: '#4d4747',
22
+ secondaryColor: '#8b5cf6',
23
+ bgColor: '#000000',
24
+ surfaceColor: '#000000',
25
+ textColor: '#f1f5f9',
26
+ textMuted: '#94a3b8',
27
+ borderColor: '#334155',
28
+ successColor: '#10b981',
29
+ warningColor: '#f59e0b',
30
+ mode: 'dark'
31
+ };
32
+ }
33
+
34
+ // Load settings from localStorage
35
+ loadSettings() {
36
+ try {
37
+ const saved = localStorage.getItem('tunecamp-theme-settings');
38
+ if (saved) {
39
+ return { ...this.getDefaults(), ...JSON.parse(saved) };
40
+ }
41
+ } catch (e) {
42
+ console.warn('Could not load theme settings:', e);
43
+ }
44
+ return this.getDefaults();
45
+ }
46
+
47
+ // Save settings to localStorage
48
+ saveSettings() {
49
+ try {
50
+ localStorage.setItem('tunecamp-theme-settings', JSON.stringify(this.settings));
51
+ } catch (e) {
52
+ console.warn('Could not save theme settings:', e);
53
+ }
54
+ }
55
+
56
+ // Apply current settings to CSS variables
57
+ applySettings() {
58
+ const root = document.documentElement;
59
+
60
+ if (this.settings.mode === 'light') {
61
+ // Light mode - use lighter variants
62
+ root.style.setProperty('--primary-color', this.settings.primaryColor);
63
+ root.style.setProperty('--secondary-color', this.settings.secondaryColor);
64
+ root.style.setProperty('--bg-color', '#f8fafc');
65
+ root.style.setProperty('--surface-color', '#ffffff');
66
+ root.style.setProperty('--text-color', '#1e293b');
67
+ root.style.setProperty('--text-muted', '#64748b');
68
+ root.style.setProperty('--border-color', '#e2e8f0');
69
+ root.setAttribute('data-theme', 'light');
70
+ } else {
71
+ // Dark mode
72
+ root.style.setProperty('--primary-color', this.settings.primaryColor);
73
+ root.style.setProperty('--secondary-color', this.settings.secondaryColor);
74
+ root.style.setProperty('--bg-color', this.settings.bgColor);
75
+ root.style.setProperty('--surface-color', this.settings.surfaceColor);
76
+ root.style.setProperty('--text-color', this.settings.textColor);
77
+ root.style.setProperty('--border-color', this.settings.borderColor);
78
+ root.setAttribute('data-theme', 'dark');
79
+ }
80
+ }
81
+
82
+ // Create the widget UI
83
+ createWidget() {
84
+ // Widget toggle button
85
+ const toggle = document.createElement('button');
86
+ toggle.className = 'theme-widget-toggle';
87
+ toggle.innerHTML = '🎨';
88
+ toggle.title = 'Customize Theme';
89
+ toggle.addEventListener('click', () => this.togglePanel());
90
+
91
+ // Widget panel
92
+ const panel = document.createElement('div');
93
+ panel.className = 'theme-widget-panel';
94
+ panel.innerHTML = `
95
+ <div class="theme-widget-header">
96
+ <span>Theme Settings</span>
97
+ <button class="theme-widget-close">&times;</button>
98
+ </div>
99
+ <div class="theme-widget-body">
100
+ <div class="theme-widget-row">
101
+ <label>Mode</label>
102
+ <select id="tw-mode">
103
+ <option value="dark">Dark</option>
104
+ <option value="light">Light</option>
105
+ </select>
106
+ </div>
107
+ <div class="theme-widget-row">
108
+ <label>Primary Color</label>
109
+ <input type="color" id="tw-primary" value="${this.settings.primaryColor}">
110
+ </div>
111
+ <div class="theme-widget-row">
112
+ <label>Secondary Color</label>
113
+ <input type="color" id="tw-secondary" value="${this.settings.secondaryColor}">
114
+ </div>
115
+ <div class="theme-widget-row dark-only">
116
+ <label>Background</label>
117
+ <input type="color" id="tw-bg" value="${this.settings.bgColor}">
118
+ </div>
119
+ <div class="theme-widget-row dark-only">
120
+ <label>Surface</label>
121
+ <input type="color" id="tw-surface" value="${this.settings.surfaceColor}">
122
+ </div>
123
+ <div class="theme-widget-actions">
124
+ <button id="tw-reset">Reset</button>
125
+ <button id="tw-export">Export CSS</button>
126
+ </div>
127
+ </div>
128
+ `;
129
+
130
+ // Add styles
131
+ const styles = document.createElement('style');
132
+ styles.textContent = `
133
+ .theme-widget-toggle {
134
+ position: fixed;
135
+ bottom: 20px;
136
+ right: 20px;
137
+ width: 50px;
138
+ height: 50px;
139
+ border-radius: 50%;
140
+ background: var(--primary-color);
141
+ border: none;
142
+ font-size: 24px;
143
+ cursor: pointer;
144
+ box-shadow: 0 4px 15px rgba(0,0,0,0.3);
145
+ z-index: 9999;
146
+ transition: transform 0.3s, box-shadow 0.3s;
147
+ }
148
+ .theme-widget-toggle:hover {
149
+ transform: scale(1.1);
150
+ box-shadow: 0 6px 20px rgba(0,0,0,0.4);
151
+ }
152
+ .theme-widget-panel {
153
+ position: fixed;
154
+ bottom: 80px;
155
+ right: 20px;
156
+ width: 280px;
157
+ background: var(--surface-color);
158
+ border: 1px solid var(--border-color);
159
+ border-radius: 12px;
160
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
161
+ z-index: 9998;
162
+ display: none;
163
+ font-family: system-ui, sans-serif;
164
+ }
165
+ .theme-widget-panel.open {
166
+ display: block;
167
+ animation: slideUp 0.2s ease;
168
+ }
169
+ @keyframes slideUp {
170
+ from { opacity: 0; transform: translateY(10px); }
171
+ to { opacity: 1; transform: translateY(0); }
172
+ }
173
+ .theme-widget-header {
174
+ display: flex;
175
+ justify-content: space-between;
176
+ align-items: center;
177
+ padding: 1rem;
178
+ border-bottom: 1px solid var(--border-color);
179
+ font-weight: 600;
180
+ color: var(--text-color);
181
+ }
182
+ .theme-widget-close {
183
+ background: none;
184
+ border: none;
185
+ font-size: 20px;
186
+ cursor: pointer;
187
+ color: var(--text-muted);
188
+ }
189
+ .theme-widget-body {
190
+ padding: 1rem;
191
+ }
192
+ .theme-widget-row {
193
+ display: flex;
194
+ justify-content: space-between;
195
+ align-items: center;
196
+ margin-bottom: 0.75rem;
197
+ }
198
+ .theme-widget-row label {
199
+ color: var(--text-color);
200
+ font-size: 0.9rem;
201
+ }
202
+ .theme-widget-row input[type="color"] {
203
+ width: 40px;
204
+ height: 30px;
205
+ border: none;
206
+ border-radius: 4px;
207
+ cursor: pointer;
208
+ }
209
+ .theme-widget-row select {
210
+ padding: 0.5rem;
211
+ border-radius: 4px;
212
+ border: 1px solid var(--border-color);
213
+ background: var(--bg-color);
214
+ color: var(--text-color);
215
+ cursor: pointer;
216
+ }
217
+ .theme-widget-actions {
218
+ display: flex;
219
+ gap: 0.5rem;
220
+ margin-top: 1rem;
221
+ padding-top: 1rem;
222
+ border-top: 1px solid var(--border-color);
223
+ }
224
+ .theme-widget-actions button {
225
+ flex: 1;
226
+ padding: 0.5rem;
227
+ border: none;
228
+ border-radius: 6px;
229
+ cursor: pointer;
230
+ font-size: 0.85rem;
231
+ }
232
+ #tw-reset {
233
+ background: var(--border-color);
234
+ color: var(--text-color);
235
+ }
236
+ #tw-export {
237
+ background: var(--primary-color);
238
+ color: white;
239
+ }
240
+ .dark-only {
241
+ display: flex;
242
+ }
243
+ [data-theme="light"] .dark-only {
244
+ display: none;
245
+ }
246
+ `;
247
+
248
+ document.head.appendChild(styles);
249
+ document.body.appendChild(toggle);
250
+ document.body.appendChild(panel);
251
+
252
+ this.toggle = toggle;
253
+ this.panel = panel;
254
+ this.bindEvents();
255
+ }
256
+
257
+ // Bind event handlers
258
+ bindEvents() {
259
+ // Close button
260
+ this.panel.querySelector('.theme-widget-close').addEventListener('click', () => {
261
+ this.togglePanel();
262
+ });
263
+
264
+ // Mode select
265
+ const modeSelect = this.panel.querySelector('#tw-mode');
266
+ modeSelect.value = this.settings.mode;
267
+ modeSelect.addEventListener('change', (e) => {
268
+ this.settings.mode = e.target.value;
269
+ this.applySettings();
270
+ this.saveSettings();
271
+ });
272
+
273
+ // Color inputs
274
+ this.bindColorInput('tw-primary', 'primaryColor');
275
+ this.bindColorInput('tw-secondary', 'secondaryColor');
276
+ this.bindColorInput('tw-bg', 'bgColor');
277
+ this.bindColorInput('tw-surface', 'surfaceColor');
278
+
279
+ // Reset button
280
+ this.panel.querySelector('#tw-reset').addEventListener('click', () => {
281
+ this.settings = this.getDefaults();
282
+ this.applySettings();
283
+ this.saveSettings();
284
+ this.updateInputs();
285
+ });
286
+
287
+ // Export button
288
+ this.panel.querySelector('#tw-export').addEventListener('click', () => {
289
+ this.exportCSS();
290
+ });
291
+ }
292
+
293
+ // Bind a color input
294
+ bindColorInput(inputId, settingKey) {
295
+ const input = this.panel.querySelector(`#${inputId}`);
296
+ if (input) {
297
+ input.value = this.settings[settingKey];
298
+ input.addEventListener('input', (e) => {
299
+ this.settings[settingKey] = e.target.value;
300
+ this.applySettings();
301
+ this.saveSettings();
302
+ });
303
+ }
304
+ }
305
+
306
+ // Update all input values
307
+ updateInputs() {
308
+ const inputs = {
309
+ 'tw-mode': 'mode',
310
+ 'tw-primary': 'primaryColor',
311
+ 'tw-secondary': 'secondaryColor',
312
+ 'tw-bg': 'bgColor',
313
+ 'tw-surface': 'surfaceColor'
314
+ };
315
+
316
+ for (const [id, key] of Object.entries(inputs)) {
317
+ const el = this.panel.querySelector(`#${id}`);
318
+ if (el) el.value = this.settings[key];
319
+ }
320
+ }
321
+
322
+ // Toggle panel visibility
323
+ togglePanel() {
324
+ this.isOpen = !this.isOpen;
325
+ this.panel.classList.toggle('open', this.isOpen);
326
+ }
327
+
328
+ // Export current theme as CSS
329
+ exportCSS() {
330
+ const css = `:root {
331
+ --primary-color: ${this.settings.primaryColor};
332
+ --secondary-color: ${this.settings.secondaryColor};
333
+ --bg-color: ${this.settings.bgColor};
334
+ --surface-color: ${this.settings.surfaceColor};
335
+ }`;
336
+
337
+ // Copy to clipboard
338
+ navigator.clipboard.writeText(css).then(() => {
339
+ alert('CSS copied to clipboard!');
340
+ }).catch(() => {
341
+ // Fallback: show in prompt
342
+ prompt('Copy this CSS:', css);
343
+ });
344
+ }
345
+ }
346
+
347
+ // Initialize widget when DOM is ready
348
+ if (document.readyState === 'loading') {
349
+ document.addEventListener('DOMContentLoaded', () => new ThemeWidget());
350
+ } else {
351
+ new ThemeWidget();
352
+ }
353
+ })();
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Tunecamp Unlock Codes - GunDB Client
3
+ * Decentralized unlock code validation using GunDB public peers
4
+ *
5
+ * Usage for beginners:
6
+ * 1. Include GunDB: <script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
7
+ * 2. Include this script
8
+ * 3. Initialize: const unlockCodes = new TunecampUnlockCodes();
9
+ * 4. Validate: unlockCodes.validateCode(releaseSlug, userCode).then(valid => {...})
10
+ */
11
+
12
+ (function() {
13
+ 'use strict';
14
+
15
+ // Default public GunDB peers
16
+ const DEFAULT_PEERS = [
17
+ 'https://gun-manhattan.herokuapp.com/gun',
18
+ 'https://gun-us.herokuapp.com/gun',
19
+ ];
20
+
21
+ /**
22
+ * TunecampUnlockCodes class
23
+ * Handles code validation and redemption via GunDB
24
+ */
25
+ class TunecampUnlockCodes {
26
+ /**
27
+ * Initialize the unlock codes system
28
+ * @param {Object} options - Configuration options
29
+ * @param {Array} options.peers - GunDB peer URLs (default: public peers)
30
+ * @param {string} options.namespace - GunDB namespace (default: 'tunecamp')
31
+ */
32
+ constructor(options = {}) {
33
+ this.peers = options.peers || DEFAULT_PEERS;
34
+ this.namespace = options.namespace || 'tunecamp';
35
+ this.gun = null;
36
+ this.initialized = false;
37
+
38
+ // Initialize GunDB when script loads
39
+ this.init();
40
+ }
41
+
42
+ /**
43
+ * Initialize GunDB connection
44
+ */
45
+ async init() {
46
+ // Check if Gun is available
47
+ if (typeof Gun === 'undefined') {
48
+ console.warn('GunDB not loaded. Please include gun.js before unlock-codes.js');
49
+ return;
50
+ }
51
+
52
+ // Initialize Gun with peers
53
+ this.gun = Gun({
54
+ peers: this.peers,
55
+ localStorage: true, // Use localStorage for offline caching
56
+ });
57
+
58
+ this.initialized = true;
59
+ console.log('🔐 Tunecamp Unlock Codes initialized');
60
+ }
61
+
62
+ /**
63
+ * Wait for Gun to be ready
64
+ */
65
+ async waitForInit() {
66
+ if (this.initialized) return;
67
+
68
+ return new Promise((resolve) => {
69
+ const check = () => {
70
+ if (this.initialized) {
71
+ resolve();
72
+ } else {
73
+ setTimeout(check, 100);
74
+ }
75
+ };
76
+ check();
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Hash a code for secure storage
82
+ * @param {string} code - The unlock code
83
+ * @returns {string} Hashed code
84
+ */
85
+ async hashCode(code) {
86
+ const encoder = new TextEncoder();
87
+ const data = encoder.encode(code.toLowerCase().trim());
88
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
89
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
90
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
91
+ }
92
+
93
+ /**
94
+ * Validate an unlock code
95
+ * @param {string} releaseSlug - The release identifier
96
+ * @param {string} code - The unlock code entered by user
97
+ * @returns {Promise<Object>} Validation result
98
+ */
99
+ async validateCode(releaseSlug, code) {
100
+ await this.waitForInit();
101
+
102
+ if (!this.gun) {
103
+ return { valid: false, error: 'GunDB not initialized' };
104
+ }
105
+
106
+ const codeHash = await this.hashCode(code);
107
+
108
+ return new Promise((resolve) => {
109
+ this.gun
110
+ .get(this.namespace)
111
+ .get('releases')
112
+ .get(releaseSlug)
113
+ .get('codes')
114
+ .get(codeHash)
115
+ .once((data) => {
116
+ if (!data) {
117
+ resolve({ valid: false, error: 'Invalid code' });
118
+ return;
119
+ }
120
+
121
+ if (data.used) {
122
+ resolve({ valid: false, error: 'Code already used' });
123
+ return;
124
+ }
125
+
126
+ resolve({
127
+ valid: true,
128
+ data: {
129
+ maxDownloads: data.maxDownloads || 1,
130
+ currentDownloads: data.downloads || 0,
131
+ expiresAt: data.expiresAt,
132
+ }
133
+ });
134
+ });
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Redeem a code (mark as used)
140
+ * @param {string} releaseSlug - The release identifier
141
+ * @param {string} code - The unlock code
142
+ * @returns {Promise<Object>} Redemption result
143
+ */
144
+ async redeemCode(releaseSlug, code) {
145
+ await this.waitForInit();
146
+
147
+ if (!this.gun) {
148
+ return { success: false, error: 'GunDB not initialized' };
149
+ }
150
+
151
+ const codeHash = await this.hashCode(code);
152
+
153
+ return new Promise((resolve) => {
154
+ const codeRef = this.gun
155
+ .get(this.namespace)
156
+ .get('releases')
157
+ .get(releaseSlug)
158
+ .get('codes')
159
+ .get(codeHash);
160
+
161
+ codeRef.once((data) => {
162
+ if (!data) {
163
+ resolve({ success: false, error: 'Invalid code' });
164
+ return;
165
+ }
166
+
167
+ if (data.used) {
168
+ resolve({ success: false, error: 'Code already used' });
169
+ return;
170
+ }
171
+
172
+ // Check max downloads
173
+ const downloads = (data.downloads || 0) + 1;
174
+ const maxDownloads = data.maxDownloads || 1;
175
+ const used = downloads >= maxDownloads;
176
+
177
+ // Update the code
178
+ codeRef.put({
179
+ ...data,
180
+ downloads,
181
+ used,
182
+ lastUsedAt: Date.now(),
183
+ });
184
+
185
+ resolve({
186
+ success: true,
187
+ downloadsRemaining: maxDownloads - downloads
188
+ });
189
+ });
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Get release info (for artists to check their codes)
195
+ * @param {string} releaseSlug - The release identifier
196
+ */
197
+ async getReleaseInfo(releaseSlug) {
198
+ await this.waitForInit();
199
+
200
+ return new Promise((resolve) => {
201
+ const codes = [];
202
+
203
+ this.gun
204
+ .get(this.namespace)
205
+ .get('releases')
206
+ .get(releaseSlug)
207
+ .get('codes')
208
+ .map()
209
+ .once((data, key) => {
210
+ if (data) {
211
+ codes.push({ hash: key, ...data });
212
+ }
213
+ });
214
+
215
+ // Give it some time to collect all codes
216
+ setTimeout(() => {
217
+ resolve(codes);
218
+ }, 1000);
219
+ });
220
+ }
221
+ }
222
+
223
+ // Expose globally
224
+ window.TunecampUnlockCodes = TunecampUnlockCodes;
225
+ })();