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,553 @@
1
+ <div class="container">
2
+ <nav class="breadcrumb">
3
+ <a href="{{path backUrl}}">← Back to catalog</a>
4
+ </nav>
5
+
6
+ <!-- Share & Embed Section -->
7
+ <section class="release-share-section">
8
+ <div class="share-actions">
9
+ <h3>Share & Embed</h3>
10
+ <div class="share-buttons">
11
+ <a href="{{path backUrl}}" class="share-btn" title="View Catalog">
12
+ <i class="fas fa-list"></i> Catalog
13
+ </a>
14
+ <a href="{{assetPath 'feed.xml'}}" class="share-btn" title="RSS Feed" target="_blank">
15
+ <i class="fas fa-rss"></i> RSS Feed
16
+ </a>
17
+ <a href="{{assetPath 'atom.xml'}}" class="share-btn" title="Atom Feed" target="_blank">
18
+ <i class="fas fa-rss-square"></i> Atom Feed
19
+ </a>
20
+ <button class="share-btn" onclick="copyShareLink()" title="Copy Share Link">
21
+ <i class="fas fa-link"></i> Copy Link
22
+ </button>
23
+ <button class="share-btn" onclick="showEmbedCode()" title="Show Embed Code">
24
+ <i class="fas fa-code"></i> Embed Code
25
+ </button>
26
+ </div>
27
+ </div>
28
+ </section>
29
+
30
+ <!-- Embed Code Modal -->
31
+ <div id="embedModal" class="embed-modal" style="display: none;">
32
+ <div class="embed-modal-content">
33
+ <div class="embed-modal-header">
34
+ <h3>Embed Code</h3>
35
+ <button class="embed-modal-close" onclick="closeEmbedModal()">&times;</button>
36
+ </div>
37
+ <div class="embed-modal-body">
38
+ <div class="embed-tabs">
39
+ <button class="embed-tab active" onclick="switchEmbedTab('full')">Full Embed</button>
40
+ <button class="embed-tab" onclick="switchEmbedTab('compact')">Compact</button>
41
+ </div>
42
+ <div id="embedCodeContent" class="embed-code-content">
43
+ <textarea id="embedCodeText" readonly class="embed-code-text"></textarea>
44
+ <button class="btn btn-primary" onclick="copyEmbedCode()">
45
+ <i class="fas fa-copy"></i> Copy Code
46
+ </button>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <article class="release-detail">
53
+ <header class="release-header">
54
+ {{#if release.coverUrl}}
55
+ <div class="release-cover-large">
56
+ <img src="{{path release.coverUrl}}" alt="{{release.config.title}}">
57
+ </div>
58
+ {{/if}}
59
+
60
+ <div class="release-metadata">
61
+ <h1>{{release.config.title}}</h1>
62
+
63
+ {{#if artist}}
64
+ <p class="release-artist">by {{artist.name}}</p>
65
+ {{/if}}
66
+
67
+ <p class="release-date">Released {{formatDate release.config.date}}</p>
68
+
69
+ <p class="release-downloads" id="releaseDownloads" style="display: none;">
70
+ <i class="fas fa-download"></i> <span id="downloadCount">0</span> downloads
71
+ </p>
72
+
73
+ {{#if release.config.genres}}
74
+ <div class="release-genres">
75
+ {{#each release.config.genres}}
76
+ <span class="genre-tag">{{this}}</span>
77
+ {{/each}}
78
+ </div>
79
+ {{/if}}
80
+
81
+ {{#if release.config.description}}
82
+ <p class="release-description">{{release.config.description}}</p>
83
+ {{/if}}
84
+
85
+ {{#if release.config.credits}}
86
+ <div class="release-credits">
87
+ <h3>Credits</h3>
88
+ <ul>
89
+ {{#each release.config.credits}}
90
+ <li><strong>{{role}}:</strong> {{name}}</li>
91
+ {{/each}}
92
+ </ul>
93
+ </div>
94
+ {{/if}}
95
+
96
+ {{#if release.config.license}}
97
+ <div class="release-license">
98
+ <h3>License</h3>
99
+ <p class="license-info">
100
+ {{#if (eq release.config.license "copyright")}}
101
+ <i class="fas fa-copyright"></i> All rights reserved. This work is protected by copyright.
102
+ {{else if (eq release.config.license "cc-by")}}
103
+ <i class="fab fa-creative-commons"></i> Licensed under <a
104
+ href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0</a> - Attribution
105
+ {{else if (eq release.config.license "cc-by-sa")}}
106
+ <i class="fab fa-creative-commons"></i> Licensed under <a
107
+ href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC BY-SA 4.0</a> -
108
+ Attribution-ShareAlike
109
+ {{else if (eq release.config.license "cc-by-nc")}}
110
+ <i class="fab fa-creative-commons"></i> Licensed under <a
111
+ href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank">CC BY-NC 4.0</a> -
112
+ Attribution-NonCommercial
113
+ {{else if (eq release.config.license "cc-by-nc-sa")}}
114
+ <i class="fab fa-creative-commons"></i> Licensed under <a
115
+ href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> -
116
+ Attribution-NonCommercial-ShareAlike
117
+ {{else if (eq release.config.license "cc-by-nc-nd")}}
118
+ <i class="fab fa-creative-commons"></i> Licensed under <a
119
+ href="https://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank">CC BY-NC-ND 4.0</a> -
120
+ Attribution-NonCommercial-NoDerivatives
121
+ {{else if (eq release.config.license "cc-by-nd")}}
122
+ <i class="fab fa-creative-commons"></i> Licensed under <a
123
+ href="https://creativecommons.org/licenses/by-nd/4.0/" target="_blank">CC BY-ND 4.0</a> -
124
+ Attribution-NoDerivatives
125
+ {{else if (eq release.config.license "public-domain")}}
126
+ <i class="fas fa-globe"></i> Public Domain - This work is in the public domain.
127
+ {{/if}}
128
+ </p>
129
+ </div>
130
+ {{/if}}
131
+
132
+ {{#if release.config.download}}
133
+ <div class="release-download-actions">
134
+ {{#if (eq release.config.download "free")}}
135
+ <button class="btn btn-primary" onclick="downloadAll()">
136
+ <i class="fas fa-download"></i> Download All (Free)
137
+ </button>
138
+ {{else if (eq release.config.download "paycurtain")}}
139
+ <div class="paycurtain">
140
+ <div class="support-message">
141
+ <p><strong>Support the artist!</strong></p>
142
+ <p>Suggested donation: ${{release.config.price}}</p>
143
+ <p class="honor-system">This is an honor system - you can download for free, but your support helps the
144
+ artist continue creating music.</p>
145
+ </div>
146
+ <div class="payment-buttons">
147
+ {{#if release.config.paypalLink}}
148
+ <a href="{{release.config.paypalLink}}" target="_blank" class="btn btn-primary">
149
+ <i class="fab fa-paypal"></i> Support via PayPal
150
+ </a>
151
+ {{/if}}
152
+ {{#if release.config.stripeLink}}
153
+ <a href="{{release.config.stripeLink}}" target="_blank" class="btn btn-secondary">
154
+ <i class="fab fa-stripe"></i> Support via Stripe
155
+ </a>
156
+ {{/if}}
157
+ <button class="btn btn-outline" onclick="downloadAll()">
158
+ <i class="fas fa-download"></i> Download Free
159
+ </button>
160
+ {{#unless release.config.paypalLink}}
161
+ {{#unless release.config.stripeLink}}
162
+ <p class="payment-info">No payment links configured - download is free</p>
163
+ {{/unless}}
164
+ {{/unless}}
165
+ </div>
166
+ </div>
167
+ {{else if (eq release.config.download "codes")}}
168
+ <!-- Unlock Codes Section -->
169
+ <div class="unlock-codes-section" id="unlockCodesSection">
170
+ <div class="unlock-header">
171
+ <i class="fas fa-lock"></i>
172
+ <h3>Download with Unlock Code</h3>
173
+ <p>Enter your unlock code to access downloads</p>
174
+ </div>
175
+
176
+ <div class="unlock-form" id="unlockForm">
177
+ <div class="code-input-group">
178
+ <input type="text" id="unlockCodeInput" placeholder="XXXX-XXXX-XXXX" maxlength="14" autocomplete="off"
179
+ spellcheck="false">
180
+ <button class="btn btn-primary" id="validateCodeBtn">
181
+ <i class="fas fa-key"></i> Unlock
182
+ </button>
183
+ </div>
184
+ <p class="unlock-error" id="unlockError" style="display: none;"></p>
185
+ </div>
186
+
187
+ <div class="unlock-success" id="unlockSuccess" style="display: none;">
188
+ <div class="success-icon">
189
+ <i class="fas fa-check-circle"></i>
190
+ </div>
191
+ <h4>Code Validated!</h4>
192
+ <p id="unlockMessage">You can now download this release.</p>
193
+ <button class="btn btn-primary btn-lg" onclick="downloadAll()">
194
+ <i class="fas fa-download"></i> Download All Tracks
195
+ </button>
196
+ </div>
197
+
198
+ <div class="unlock-info">
199
+ <p><small><i class="fas fa-info-circle"></i> Don't have a code? Contact the artist or purchase
200
+ one.</small></p>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- GunDB Unlock Codes Script -->
205
+ <script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
206
+ <script src="{{assetPath 'assets/unlock-codes.js'}}"></script>
207
+ <script>
208
+ (function () {
209
+ const releaseSlug = '{{release.slug}}';
210
+ const namespace = '{{#if release.config.unlockCodes.namespace}}{{release.config.unlockCodes.namespace}}{{else}}tunecamp{{/if}}';
211
+
212
+ // Initialize unlock codes
213
+ let unlockCodes;
214
+
215
+ document.addEventListener('DOMContentLoaded', function () {
216
+ // Check if already unlocked
217
+ const unlockedReleases = JSON.parse(localStorage.getItem('tunecamp_unlocked') || '{}');
218
+ if (unlockedReleases[releaseSlug]) {
219
+ showSuccess('Previously unlocked!');
220
+ return;
221
+ }
222
+
223
+ // Initialize GunDB unlock codes
224
+ unlockCodes = new TunecampUnlockCodes({ namespace });
225
+
226
+ // Bind form events
227
+ const input = document.getElementById('unlockCodeInput');
228
+ const btn = document.getElementById('validateCodeBtn');
229
+
230
+ // Format code as user types
231
+ input.addEventListener('input', function (e) {
232
+ let value = e.target.value.replace(/[^A-Za-z0-9]/g, '').toUpperCase();
233
+ if (value.length > 4) value = value.slice(0, 4) + '-' + value.slice(4);
234
+ if (value.length > 9) value = value.slice(0, 9) + '-' + value.slice(9);
235
+ if (value.length > 14) value = value.slice(0, 14);
236
+ e.target.value = value;
237
+ });
238
+
239
+ // Validate on enter
240
+ input.addEventListener('keypress', function (e) {
241
+ if (e.key === 'Enter') validateCode();
242
+ });
243
+
244
+ // Validate on button click
245
+ btn.addEventListener('click', validateCode);
246
+ });
247
+
248
+ async function validateCode() {
249
+ const input = document.getElementById('unlockCodeInput');
250
+ const error = document.getElementById('unlockError');
251
+ const btn = document.getElementById('validateCodeBtn');
252
+ const code = input.value.trim();
253
+
254
+ if (code.length < 14) {
255
+ showError('Please enter a complete code (format: XXXX-XXXX-XXXX)');
256
+ return;
257
+ }
258
+
259
+ btn.disabled = true;
260
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Validating...';
261
+ error.style.display = 'none';
262
+
263
+ try {
264
+ const result = await unlockCodes.validateCode(releaseSlug, code);
265
+
266
+ if (result.valid) {
267
+ // Redeem the code
268
+ const redeemResult = await unlockCodes.redeemCode(releaseSlug, code);
269
+
270
+ if (redeemResult.success) {
271
+ // Store in localStorage
272
+ const unlockedReleases = JSON.parse(localStorage.getItem('tunecamp_unlocked') || '{}');
273
+ unlockedReleases[releaseSlug] = { code, unlockedAt: Date.now() };
274
+ localStorage.setItem('tunecamp_unlocked', JSON.stringify(unlockedReleases));
275
+
276
+ const msg = redeemResult.downloadsRemaining > 0
277
+ ? `Downloads remaining on this code: ${redeemResult.downloadsRemaining}`
278
+ : 'This was the last use of this code.';
279
+ showSuccess(msg);
280
+ } else {
281
+ showError(redeemResult.error || 'Failed to redeem code');
282
+ }
283
+ } else {
284
+ showError(result.error || 'Invalid code');
285
+ }
286
+ } catch (err) {
287
+ console.error('Validation error:', err);
288
+ showError('Network error. Please try again.');
289
+ } finally {
290
+ btn.disabled = false;
291
+ btn.innerHTML = '<i class="fas fa-key"></i> Unlock';
292
+ }
293
+ }
294
+
295
+ function showError(message) {
296
+ const error = document.getElementById('unlockError');
297
+ error.textContent = message;
298
+ error.style.display = 'block';
299
+ }
300
+
301
+ function showSuccess(message) {
302
+ document.getElementById('unlockForm').style.display = 'none';
303
+ document.getElementById('unlockMessage').textContent = message;
304
+ document.getElementById('unlockSuccess').style.display = 'block';
305
+ }
306
+ })();
307
+ </script>
308
+ {{/if}}
309
+ </div>
310
+ {{/if}}
311
+ </div>
312
+ </header>
313
+
314
+ <section class="tracklist">
315
+ <h2>Tracks</h2>
316
+
317
+ <div class="audio-player" id="audioPlayer">
318
+ <div class="player-track-info">
319
+ <div class="player-cover">
320
+ {{#if release.coverUrl}}
321
+ <img src="{{release.coverUrl}}" alt="{{release.config.title}}" id="playerCover">
322
+ {{else}}
323
+ <i class="fas fa-music"></i>
324
+ {{/if}}
325
+ </div>
326
+ <div class="player-details">
327
+ <div class="player-title" id="playerTitle">Select a track</div>
328
+ <div class="player-artist" id="playerArtist">{{#if artist}}{{artist.name}}{{/if}}</div>
329
+ </div>
330
+ </div>
331
+
332
+ <div class="player-controls">
333
+ <button class="player-btn" id="prevBtn"><i class="fas fa-step-backward"></i></button>
334
+ <button class="player-btn player-btn-play" id="playBtn">
335
+ <i class="fas fa-play"></i>
336
+ </button>
337
+ <button class="player-btn" id="nextBtn"><i class="fas fa-step-forward"></i></button>
338
+ </div>
339
+
340
+ <div class="player-progress">
341
+ <span class="player-time" id="currentTime">0:00</span>
342
+ <input type="range" class="progress-bar" id="progressBar" min="0" max="100" value="0">
343
+ <span class="player-time" id="duration">0:00</span>
344
+ </div>
345
+
346
+ <div class="player-volume">
347
+ <i class="fas fa-volume-up"></i>
348
+ <input type="range" class="volume-bar" id="volumeBar" min="0" max="100" value="80">
349
+ </div>
350
+ </div>
351
+
352
+ <ol class="track-list">
353
+ {{#each release.tracks}}
354
+ <li class="track-item" data-src="{{releasePath url}}" data-index="{{@index}}">
355
+ <div class="track-number">{{@index}}</div>
356
+ <div class="track-info">
357
+ <div class="track-title">{{title}}</div>
358
+ <div class="track-meta">
359
+ {{formatAudioFormat filename}}
360
+ {{#if duration}}
361
+ · {{formatDuration duration}}
362
+ {{/if}}
363
+ </div>
364
+ </div>
365
+ <div class="track-actions">
366
+ <button class="track-play-btn" onclick="playTrack({{@index}})">
367
+ <i class="fas fa-play"></i>
368
+ </button>
369
+ {{#if ../release.config.download}}
370
+ {{#if (eq ../release.config.download "free")}}
371
+ <a href="{{releasePath url}}" download class="track-download-btn">
372
+ <i class="fas fa-download"></i>
373
+ </a>
374
+ {{/if}}
375
+ {{/if}}
376
+ </div>
377
+ </li>
378
+ {{/each}}
379
+ </ol>
380
+ </section>
381
+
382
+ {{#if artist.donationLinks}}
383
+ <section class="donation-section">
384
+ <h2>Support the Artist</h2>
385
+ <p>If you enjoyed this music, consider supporting the artist:</p>
386
+ <div class="donation-links">
387
+ {{#each artist.donationLinks}}
388
+ <a href="{{url}}" target="_blank" class="donation-link">
389
+ <i class="fas fa-heart"></i>
390
+ <span>{{platform}}</span>
391
+ {{#if description}}
392
+ <small>{{description}}</small>
393
+ {{/if}}
394
+ </a>
395
+ {{/each}}
396
+ </div>
397
+ </section>
398
+ {{/if}}
399
+ </article>
400
+ </div>
401
+
402
+ <script>
403
+ // Share & Embed functionality
404
+ function copyShareLink() {
405
+ const url = window.location.href;
406
+ navigator.clipboard.writeText(url).then(() => {
407
+ alert('Link copied to clipboard!');
408
+ }).catch(() => {
409
+ // Fallback for older browsers
410
+ const textarea = document.createElement('textarea');
411
+ textarea.value = url;
412
+ document.body.appendChild(textarea);
413
+ textarea.select();
414
+ document.execCommand('copy');
415
+ document.body.removeChild(textarea);
416
+ alert('Link copied to clipboard!');
417
+ });
418
+ }
419
+
420
+ function showEmbedCode() {
421
+ const modal = document.getElementById('embedModal');
422
+ const embedCodeText = document.getElementById('embedCodeText');
423
+
424
+ // Load full embed code by default
425
+ fetch('{{path embedCodePath}}')
426
+ .then(response => response.text())
427
+ .then(text => {
428
+ embedCodeText.value = text.trim();
429
+ modal.style.display = 'flex';
430
+ })
431
+ .catch(err => {
432
+ console.error('Error loading embed code:', err);
433
+ alert('Error loading embed code');
434
+ });
435
+ }
436
+
437
+ function switchEmbedTab(type) {
438
+ const tabs = document.querySelectorAll('.embed-tab');
439
+ tabs.forEach(tab => tab.classList.remove('active'));
440
+ event.target.classList.add('active');
441
+
442
+ const embedCodeText = document.getElementById('embedCodeText');
443
+ const file = type === 'full' ? '{{embedCodePath}}' : '{{embedCompactPath}}';
444
+
445
+ fetch('{{path ""}}' + file)
446
+ .then(response => response.text())
447
+ .then(text => {
448
+ embedCodeText.value = text.trim();
449
+ })
450
+ .catch(err => {
451
+ console.error('Error loading embed code:', err);
452
+ });
453
+ }
454
+
455
+ function copyEmbedCode() {
456
+ const embedCodeText = document.getElementById('embedCodeText');
457
+ embedCodeText.select();
458
+ document.execCommand('copy');
459
+ alert('Embed code copied to clipboard!');
460
+ }
461
+
462
+ function closeEmbedModal() {
463
+ document.getElementById('embedModal').style.display = 'none';
464
+ }
465
+
466
+ // Close modal when clicking outside
467
+ window.onclick = function(event) {
468
+ const modal = document.getElementById('embedModal');
469
+ if (event.target === modal) {
470
+ modal.style.display = 'none';
471
+ }
472
+ }
473
+ </script>
474
+
475
+ <script>
476
+ // Initialize player with tracks
477
+ window.tracks = [
478
+ {{#each release.tracks}}
479
+ {
480
+ url: '{{releasePath url}}',
481
+ title: '{{title}}',
482
+ artist: '{{#if artist}}{{artist}}{{else}}{{../artist.name}}{{/if}}',
483
+ duration: {{#if duration}}{{duration}}{{else}}0{{/if}}
484
+ }{{#unless @last}},{{/unless}}
485
+ {{/each}}
486
+ ];
487
+ </script>
488
+
489
+ <!-- Download Stats (GunDB) -->
490
+ <script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
491
+ <script src="{{assetPath 'assets/download-stats.js'}}"></script>
492
+ <script>
493
+ (function() {
494
+ const releaseSlug = '{{release.slug}}';
495
+ let downloadStats;
496
+
497
+ document.addEventListener('DOMContentLoaded', function() {
498
+ // Initialize download stats
499
+ downloadStats = new TunecampDownloadStats();
500
+ window.downloadStats = downloadStats;
501
+
502
+ // Load and display current download count
503
+ loadDownloadCount();
504
+
505
+ // Subscribe to real-time updates
506
+ downloadStats.subscribeToDownloadCount(releaseSlug, updateDownloadDisplay);
507
+ });
508
+
509
+ async function loadDownloadCount() {
510
+ try {
511
+ const count = await downloadStats.getDownloadCount(releaseSlug);
512
+ updateDownloadDisplay(count);
513
+ } catch (e) {
514
+ console.warn('Could not load download count:', e);
515
+ }
516
+ }
517
+
518
+ function updateDownloadDisplay(count) {
519
+ const el = document.getElementById('releaseDownloads');
520
+ const countEl = document.getElementById('downloadCount');
521
+ if (el && countEl) {
522
+ countEl.textContent = downloadStats.formatCount(count);
523
+ el.style.display = 'block';
524
+ }
525
+ }
526
+
527
+ // Register hook for downloadAll tracking
528
+ window.onDownloadAll = async function() {
529
+ if (downloadStats) {
530
+ try {
531
+ await downloadStats.incrementDownloadCount(releaseSlug);
532
+ } catch (e) {
533
+ console.warn('Could not track download:', e);
534
+ }
535
+ }
536
+ };
537
+
538
+ // Track individual track downloads
539
+ document.querySelectorAll('.track-download-btn').forEach((btn, index) => {
540
+ btn.addEventListener('click', async function(e) {
541
+ if (downloadStats && window.tracks && window.tracks[index]) {
542
+ try {
543
+ const trackId = window.tracks[index].title.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
544
+ await downloadStats.incrementTrackDownloadCount(releaseSlug, trackId);
545
+ await downloadStats.incrementDownloadCount(releaseSlug);
546
+ } catch (e) {
547
+ console.warn('Could not track track download:', e);
548
+ }
549
+ }
550
+ });
551
+ });
552
+ })();
553
+ </script>