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,73 @@
1
+ <div class="container">
2
+ {{#if artist}}
3
+ <section class="artist-section">
4
+ <div class="artist-header">
5
+ {{#if artist.photo}}
6
+ <img src="{{path artist.photo}}" alt="{{artist.name}}" class="artist-photo">
7
+ {{/if}}
8
+ <div class="artist-info">
9
+ <h2>{{artist.name}}</h2>
10
+ {{#if artist.bio}}
11
+ <p class="artist-bio">{{artist.bio}}</p>
12
+ {{/if}}
13
+ </div>
14
+ </div>
15
+ </section>
16
+ {{/if}}
17
+
18
+ <section class="releases-section">
19
+ <h2>Releases</h2>
20
+
21
+ {{#if releases}}
22
+ <div class="releases-grid">
23
+ {{#each releases}}
24
+ <article class="release-card">
25
+ <a href="{{path url}}" class="release-link">
26
+ {{#if coverUrl}}
27
+ <div class="release-cover">
28
+ <img src="{{path coverUrl}}" alt="{{config.title}}" loading="lazy">
29
+ </div>
30
+ {{else}}
31
+ <div class="release-cover release-cover-placeholder">
32
+ <i class="fas fa-music"></i>
33
+ </div>
34
+ {{/if}}
35
+
36
+ <div class="release-info">
37
+ <h3 class="release-title">{{config.title}}</h3>
38
+ <p class="release-date">{{formatDate config.date}}</p>
39
+
40
+ {{#if config.genres}}
41
+ <div class="release-genres">
42
+ {{#each config.genres}}
43
+ <span class="genre-tag">{{this}}</span>
44
+ {{/each}}
45
+ </div>
46
+ {{/if}}
47
+
48
+ <p class="release-tracks-count">
49
+ {{tracks.length}} track{{#unless (eq tracks.length 1)}}s{{/unless}}
50
+ </p>
51
+
52
+ {{#if config.download}}
53
+ <div class="release-download-badge">
54
+ {{#if (eq config.download "free")}}
55
+ <i class="fas fa-download"></i> Free Download
56
+ {{else if (eq config.download "paycurtain")}}
57
+ <i class="fas fa-dollar-sign"></i> Pay What You Want
58
+ {{else if (eq config.download "codes")}}
59
+ <i class="fas fa-key"></i> Download Codes
60
+ {{/if}}
61
+ </div>
62
+ {{/if}}
63
+ </div>
64
+ </a>
65
+ </article>
66
+ {{/each}}
67
+ </div>
68
+ {{else}}
69
+ <p class="no-releases">No releases yet. Check back soon!</p>
70
+ {{/if}}
71
+ </section>
72
+ </div>
73
+
@@ -0,0 +1,84 @@
1
+ <!DOCTYPE html>
2
+ <html lang="{{catalog.language}}">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{#if pageTitle}}{{pageTitle}} - {{/if}}{{catalog.title}}</title>
7
+ {{#if catalog.description}}
8
+ <meta name="description" content="{{catalog.description}}">
9
+ {{/if}}
10
+ {{#if catalog.customFontUrl}}
11
+ <link rel="stylesheet" href="{{catalog.customFontUrl}}">
12
+ {{/if}}
13
+ <link rel="stylesheet" href="{{assetPath "assets/style.css"}}">
14
+ {{#if catalog.customFontFamily}}
15
+ <style>
16
+ :root {
17
+ --custom-font-family: '{{catalog.customFontFamily}}', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
18
+ }
19
+ body {
20
+ font-family: var(--custom-font-family);
21
+ }
22
+ </style>
23
+ {{/if}}
24
+ {{#if catalog.customCSSUrl}}
25
+ {{#if (or (startsWith catalog.customCSSUrl "http://") (startsWith catalog.customCSSUrl "https://"))}}
26
+ <link rel="stylesheet" href="{{catalog.customCSSUrl}}">
27
+ {{else}}
28
+ <link rel="stylesheet" href="{{path catalog.customCSSUrl}}">
29
+ {{/if}}
30
+ {{/if}}
31
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
32
+ </head>
33
+ <body>
34
+ <header class="site-header">
35
+ <div class="container">
36
+ {{#if catalog.headerImageUrl}}
37
+ <div class="site-header-image">
38
+ <a href="{{#if backUrl}}{{path backUrl}}{{else}}{{path "index.html"}}{{/if}}">
39
+ <img src="{{path catalog.headerImageUrl}}" alt="{{catalog.title}}" class="header-image">
40
+ </a>
41
+ </div>
42
+ {{else}}
43
+ <h1 class="site-title">
44
+ <a href="{{#if backUrl}}{{path backUrl}}{{else}}{{path "index.html"}}{{/if}}">{{catalog.title}}</a>
45
+ </h1>
46
+ {{/if}}
47
+ {{#if catalog.description}}
48
+ {{#unless catalog.headerImageUrl}}
49
+ <p class="site-description">{{catalog.description}}</p>
50
+ {{/unless}}
51
+ {{/if}}
52
+ </div>
53
+ </header>
54
+
55
+ <main class="site-main">
56
+ {{{content}}}
57
+ </main>
58
+
59
+ <footer class="site-footer">
60
+ <div class="container">
61
+ <p>
62
+ {{#if artist}}
63
+ &copy; {{artist.name}} -
64
+ {{/if}}
65
+ Powered by <a href="https://github.com/scobru/tunecamp" target="_blank">Tunecamp</a>
66
+ </p>
67
+ {{#if artist.links}}
68
+ <div class="social-links">
69
+ {{#each artist.links}}
70
+ {{#each this}}
71
+ <a href="{{this}}" target="_blank" rel="noopener" class="social-link">
72
+ <i class="fab fa-{{@key}}"></i>
73
+ </a>
74
+ {{/each}}
75
+ {{/each}}
76
+ </div>
77
+ {{/if}}
78
+ </div>
79
+ </footer>
80
+
81
+ <script src="{{assetPath "assets/player.js"}}"></script>
82
+ </body>
83
+ </html>
84
+
@@ -0,0 +1,212 @@
1
+ <div class="container">
2
+ <nav class="breadcrumb">
3
+ <a href="{{path backUrl}}">← Back to catalog</a>
4
+ </nav>
5
+
6
+ <article class="release-detail">
7
+ <header class="release-header">
8
+ {{#if release.coverUrl}}
9
+ <div class="release-cover-large">
10
+ <img src="{{path release.coverUrl}}" alt="{{release.config.title}}">
11
+ </div>
12
+ {{/if}}
13
+
14
+ <div class="release-metadata">
15
+ <h1>{{release.config.title}}</h1>
16
+
17
+ {{#if artist}}
18
+ <p class="release-artist">by {{artist.name}}</p>
19
+ {{/if}}
20
+
21
+ <p class="release-date">Released {{formatDate release.config.date}}</p>
22
+
23
+ {{#if release.config.genres}}
24
+ <div class="release-genres">
25
+ {{#each release.config.genres}}
26
+ <span class="genre-tag">{{this}}</span>
27
+ {{/each}}
28
+ </div>
29
+ {{/if}}
30
+
31
+ {{#if release.config.description}}
32
+ <p class="release-description">{{release.config.description}}</p>
33
+ {{/if}}
34
+
35
+ {{#if release.config.credits}}
36
+ <div class="release-credits">
37
+ <h3>Credits</h3>
38
+ <ul>
39
+ {{#each release.config.credits}}
40
+ <li><strong>{{role}}:</strong> {{name}}</li>
41
+ {{/each}}
42
+ </ul>
43
+ </div>
44
+ {{/if}}
45
+
46
+ {{#if release.config.license}}
47
+ <div class="release-license">
48
+ <h3>License</h3>
49
+ <p class="license-info">
50
+ {{#if (eq release.config.license "copyright")}}
51
+ <i class="fas fa-copyright"></i> All rights reserved. This work is protected by copyright.
52
+ {{else if (eq release.config.license "cc-by")}}
53
+ <i class="fab fa-creative-commons"></i> Licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0</a> - Attribution
54
+ {{else if (eq release.config.license "cc-by-sa")}}
55
+ <i class="fab fa-creative-commons"></i> Licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC BY-SA 4.0</a> - Attribution-ShareAlike
56
+ {{else if (eq release.config.license "cc-by-nc")}}
57
+ <i class="fab fa-creative-commons"></i> Licensed under <a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank">CC BY-NC 4.0</a> - Attribution-NonCommercial
58
+ {{else if (eq release.config.license "cc-by-nc-sa")}}
59
+ <i class="fab fa-creative-commons"></i> Licensed under <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> - Attribution-NonCommercial-ShareAlike
60
+ {{else if (eq release.config.license "cc-by-nc-nd")}}
61
+ <i class="fab fa-creative-commons"></i> Licensed under <a href="https://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank">CC BY-NC-ND 4.0</a> - Attribution-NonCommercial-NoDerivatives
62
+ {{else if (eq release.config.license "cc-by-nd")}}
63
+ <i class="fab fa-creative-commons"></i> Licensed under <a href="https://creativecommons.org/licenses/by-nd/4.0/" target="_blank">CC BY-ND 4.0</a> - Attribution-NoDerivatives
64
+ {{else if (eq release.config.license "public-domain")}}
65
+ <i class="fas fa-globe"></i> Public Domain - This work is in the public domain.
66
+ {{/if}}
67
+ </p>
68
+ </div>
69
+ {{/if}}
70
+
71
+ {{#if release.config.download}}
72
+ <div class="release-download-actions">
73
+ {{#if (eq release.config.download "free")}}
74
+ <button class="btn btn-primary" onclick="downloadAll()">
75
+ <i class="fas fa-download"></i> Download All (Free)
76
+ </button>
77
+ {{else if (eq release.config.download "paycurtain")}}
78
+ <div class="paycurtain">
79
+ <div class="support-message">
80
+ <p><strong>Support the artist!</strong></p>
81
+ <p>Suggested donation: ${{release.config.price}}</p>
82
+ <p class="honor-system">This is an honor system - you can download for free, but your support helps the artist continue creating music.</p>
83
+ </div>
84
+ <div class="payment-buttons">
85
+ {{#if release.config.paypalLink}}
86
+ <a href="{{release.config.paypalLink}}" target="_blank" class="btn btn-primary">
87
+ <i class="fab fa-paypal"></i> Support via PayPal
88
+ </a>
89
+ {{/if}}
90
+ {{#if release.config.stripeLink}}
91
+ <a href="{{release.config.stripeLink}}" target="_blank" class="btn btn-secondary">
92
+ <i class="fab fa-stripe"></i> Support via Stripe
93
+ </a>
94
+ {{/if}}
95
+ <button class="btn btn-outline" onclick="downloadAll()">
96
+ <i class="fas fa-download"></i> Download Free
97
+ </button>
98
+ {{#unless release.config.paypalLink}}
99
+ {{#unless release.config.stripeLink}}
100
+ <p class="payment-info">No payment links configured - download is free</p>
101
+ {{/unless}}
102
+ {{/unless}}
103
+ </div>
104
+ </div>
105
+ {{/if}}
106
+ </div>
107
+ {{/if}}
108
+ </div>
109
+ </header>
110
+
111
+ <section class="tracklist">
112
+ <h2>Tracks</h2>
113
+
114
+ <div class="audio-player" id="audioPlayer">
115
+ <div class="player-track-info">
116
+ <div class="player-cover">
117
+ {{#if release.coverUrl}}
118
+ <img src="{{release.coverUrl}}" alt="{{release.config.title}}" id="playerCover">
119
+ {{else}}
120
+ <i class="fas fa-music"></i>
121
+ {{/if}}
122
+ </div>
123
+ <div class="player-details">
124
+ <div class="player-title" id="playerTitle">Select a track</div>
125
+ <div class="player-artist" id="playerArtist">{{#if artist}}{{artist.name}}{{/if}}</div>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="player-controls">
130
+ <button class="player-btn" id="prevBtn"><i class="fas fa-step-backward"></i></button>
131
+ <button class="player-btn player-btn-play" id="playBtn">
132
+ <i class="fas fa-play"></i>
133
+ </button>
134
+ <button class="player-btn" id="nextBtn"><i class="fas fa-step-forward"></i></button>
135
+ </div>
136
+
137
+ <div class="player-progress">
138
+ <span class="player-time" id="currentTime">0:00</span>
139
+ <input type="range" class="progress-bar" id="progressBar" min="0" max="100" value="0">
140
+ <span class="player-time" id="duration">0:00</span>
141
+ </div>
142
+
143
+ <div class="player-volume">
144
+ <i class="fas fa-volume-up"></i>
145
+ <input type="range" class="volume-bar" id="volumeBar" min="0" max="100" value="80">
146
+ </div>
147
+ </div>
148
+
149
+ <ol class="track-list">
150
+ {{#each release.tracks}}
151
+ <li class="track-item" data-src="{{releasePath url}}" data-index="{{@index}}">
152
+ <div class="track-number">{{@index}}</div>
153
+ <div class="track-info">
154
+ <div class="track-title">{{title}}</div>
155
+ <div class="track-meta">
156
+ {{formatAudioFormat filename}}
157
+ {{#if duration}}
158
+ · {{formatDuration duration}}
159
+ {{/if}}
160
+ </div>
161
+ </div>
162
+ <div class="track-actions">
163
+ <button class="track-play-btn" onclick="playTrack({{@index}})">
164
+ <i class="fas fa-play"></i>
165
+ </button>
166
+ {{#if ../release.config.download}}
167
+ {{#if (eq ../release.config.download "free")}}
168
+ <a href="{{releasePath url}}" download class="track-download-btn">
169
+ <i class="fas fa-download"></i>
170
+ </a>
171
+ {{/if}}
172
+ {{/if}}
173
+ </div>
174
+ </li>
175
+ {{/each}}
176
+ </ol>
177
+ </section>
178
+
179
+ {{#if artist.donationLinks}}
180
+ <section class="donation-section">
181
+ <h2>Support the Artist</h2>
182
+ <p>If you enjoyed this music, consider supporting the artist:</p>
183
+ <div class="donation-links">
184
+ {{#each artist.donationLinks}}
185
+ <a href="{{url}}" target="_blank" class="donation-link">
186
+ <i class="fas fa-heart"></i>
187
+ <span>{{platform}}</span>
188
+ {{#if description}}
189
+ <small>{{description}}</small>
190
+ {{/if}}
191
+ </a>
192
+ {{/each}}
193
+ </div>
194
+ </section>
195
+ {{/if}}
196
+ </article>
197
+ </div>
198
+
199
+ <script>
200
+ // Initialize player with tracks
201
+ window.tracks = [
202
+ {{#each release.tracks}}
203
+ {
204
+ url: '{{releasePath url}}',
205
+ title: '{{title}}',
206
+ artist: '{{#if artist}}{{artist}}{{else}}{{../artist.name}}{{/if}}',
207
+ duration: {{#if duration}}{{duration}}{{else}}0{{/if}}
208
+ }{{#unless @last}},{{/unless}}
209
+ {{/each}}
210
+ ];
211
+ </script>
212
+
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Tunecamp Community Registry
3
+ * Auto-registers Tunecamp sites to a global public GunDB registry
4
+ *
5
+ * When a visitor loads any Tunecamp site, it automatically registers
6
+ * the site in a decentralized community directory.
7
+ */
8
+
9
+ (function() {
10
+ 'use strict';
11
+
12
+ // Public GunDB peers for the community registry
13
+ const REGISTRY_PEERS = [
14
+ 'https://gun.defucc.me/gun',
15
+ 'https://gun.o8.is/gun',
16
+ 'https://shogun-relay.scobrudot.dev/gun',
17
+ 'https://relay.peer.ooo/gun',
18
+ ];
19
+
20
+ const REGISTRY_ROOT = 'shogun';
21
+ const REGISTRY_NAMESPACE = 'tunecamp-community';
22
+ const REGISTRY_VERSION = '1.0';
23
+
24
+ /**
25
+ * TunecampCommunityRegistry
26
+ * Handles auto-registration and discovery of Tunecamp sites
27
+ */
28
+ class TunecampCommunityRegistry {
29
+ constructor() {
30
+ this.gun = null;
31
+ this.initialized = false;
32
+ this.siteData = null;
33
+ }
34
+
35
+ /**
36
+ * Initialize GunDB connection
37
+ */
38
+ async init() {
39
+ if (typeof Gun === 'undefined') {
40
+ console.warn('GunDB not loaded. Community registry disabled.');
41
+ return false;
42
+ }
43
+
44
+ this.gun = Gun({
45
+ peers: REGISTRY_PEERS,
46
+ localStorage: true,
47
+ });
48
+
49
+ this.initialized = true;
50
+ console.log('🌐 Tunecamp Community Registry initialized');
51
+ return true;
52
+ }
53
+
54
+ /**
55
+ * Generate a unique site ID from title + artist (content-based, not URL-based)
56
+ * This prevents duplicates when the same site is deployed to multiple URLs (e.g., Vercel previews)
57
+ */
58
+ generateSiteId(siteInfo) {
59
+ // Use title + artist as the unique identifier
60
+ // This way the same site deployed to different URLs won't create duplicates
61
+ const identifier = `${(siteInfo.title || 'untitled').toLowerCase().trim()}::${(siteInfo.artistName || 'unknown').toLowerCase().trim()}`;
62
+
63
+ // Create a simple hash
64
+ let hash = 0;
65
+ for (let i = 0; i < identifier.length; i++) {
66
+ const char = identifier.charCodeAt(i);
67
+ hash = ((hash << 5) - hash) + char;
68
+ hash = hash & hash;
69
+ }
70
+ return Math.abs(hash).toString(36);
71
+ }
72
+
73
+ /**
74
+ * Register current site in the community registry
75
+ * @param {Object} siteInfo - Site information
76
+ * @param {string} siteInfo.url - Site URL
77
+ * @param {string} siteInfo.title - Catalog/Artist title
78
+ * @param {string} siteInfo.description - Site description
79
+ * @param {string} siteInfo.artistName - Artist name (optional)
80
+ * @param {string} siteInfo.coverImage - Cover image URL (optional)
81
+ */
82
+ async registerSite(siteInfo) {
83
+ if (!this.initialized || !this.gun) {
84
+ console.warn('Registry not initialized');
85
+ return false;
86
+ }
87
+
88
+ const siteId = this.generateSiteId(siteInfo);
89
+ const now = Date.now();
90
+
91
+ // Check if already registered recently (within 24h)
92
+ const lastRegistration = localStorage.getItem('tunecamp_registered');
93
+ if (lastRegistration) {
94
+ const lastTime = parseInt(lastRegistration, 10);
95
+ if (now - lastTime < 24 * 60 * 60 * 1000) {
96
+ // Already registered recently, just update lastSeen
97
+ this.gun
98
+ .get(REGISTRY_ROOT)
99
+ .get(REGISTRY_NAMESPACE)
100
+ .get('sites')
101
+ .get(siteId)
102
+ .get('lastSeen')
103
+ .put(now);
104
+ return true;
105
+ }
106
+ }
107
+
108
+ const siteRecord = {
109
+ id: siteId,
110
+ url: siteInfo.url,
111
+ title: siteInfo.title || 'Untitled',
112
+ description: siteInfo.description || '',
113
+ artistName: siteInfo.artistName || '',
114
+ coverImage: siteInfo.coverImage || '',
115
+ registeredAt: now,
116
+ lastSeen: now,
117
+ version: REGISTRY_VERSION,
118
+ };
119
+
120
+ return new Promise((resolve) => {
121
+ this.gun
122
+ .get(REGISTRY_ROOT)
123
+ .get(REGISTRY_NAMESPACE)
124
+ .get('sites')
125
+ .get(siteId)
126
+ .put(siteRecord, (ack) => {
127
+ if (ack.err) {
128
+ console.warn('Failed to register site:', ack.err);
129
+ resolve(false);
130
+ } else {
131
+ localStorage.setItem('tunecamp_registered', now.toString());
132
+ console.log('✅ Site registered in Tunecamp Community');
133
+ resolve(true);
134
+ }
135
+ });
136
+
137
+ // Timeout fallback
138
+ setTimeout(() => resolve(true), 3000);
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Get all registered sites
144
+ * @param {function} callback - Called with array of sites
145
+ */
146
+ async getAllSites(callback) {
147
+ if (!this.initialized || !this.gun) {
148
+ callback([]);
149
+ return;
150
+ }
151
+
152
+ const sites = [];
153
+ const seenIds = new Set();
154
+
155
+ this.gun
156
+ .get(REGISTRY_ROOT)
157
+ .get(REGISTRY_NAMESPACE)
158
+ .get('sites')
159
+ .map()
160
+ .once((data, key) => {
161
+ if (data && data.url && !seenIds.has(key)) {
162
+ seenIds.add(key);
163
+ sites.push({
164
+ id: key,
165
+ url: data.url,
166
+ title: data.title || 'Untitled',
167
+ description: data.description || '',
168
+ artistName: data.artistName || '',
169
+ coverImage: data.coverImage || '',
170
+ registeredAt: data.registeredAt,
171
+ lastSeen: data.lastSeen,
172
+ });
173
+ }
174
+ });
175
+
176
+ // Give time to collect all sites
177
+ setTimeout(() => {
178
+ // Sort by lastSeen (most recent first)
179
+ sites.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
180
+ callback(sites);
181
+ }, 2000);
182
+ }
183
+
184
+ /**
185
+ * Subscribe to new sites (real-time)
186
+ * @param {function} callback - Called when a new site is added
187
+ */
188
+ subscribeToSites(callback) {
189
+ if (!this.initialized || !this.gun) {
190
+ return () => {};
191
+ }
192
+
193
+ const ref = this.gun
194
+ .get(REGISTRY_ROOT)
195
+ .get(REGISTRY_NAMESPACE)
196
+ .get('sites')
197
+ .map()
198
+ .on((data, key) => {
199
+ if (data && data.url) {
200
+ callback({
201
+ id: key,
202
+ url: data.url,
203
+ title: data.title || 'Untitled',
204
+ description: data.description || '',
205
+ artistName: data.artistName || '',
206
+ coverImage: data.coverImage || '',
207
+ registeredAt: data.registeredAt,
208
+ lastSeen: data.lastSeen,
209
+ });
210
+ }
211
+ });
212
+
213
+ return () => ref.off();
214
+ }
215
+
216
+ /**
217
+ * Format timestamp for display
218
+ */
219
+ formatDate(timestamp) {
220
+ if (!timestamp) return 'Unknown';
221
+ const date = new Date(timestamp);
222
+ return date.toLocaleDateString('en-US', {
223
+ year: 'numeric',
224
+ month: 'short',
225
+ day: 'numeric',
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Get site count
231
+ */
232
+ async getSiteCount() {
233
+ return new Promise((resolve) => {
234
+ let count = 0;
235
+
236
+ this.gun
237
+ .get(REGISTRY_ROOT)
238
+ .get(REGISTRY_NAMESPACE)
239
+ .get('sites')
240
+ .map()
241
+ .once((data) => {
242
+ if (data && data.url) count++;
243
+ });
244
+
245
+ setTimeout(() => resolve(count), 2000);
246
+ });
247
+ }
248
+ }
249
+
250
+ // Expose globally
251
+ window.TunecampCommunityRegistry = TunecampCommunityRegistry;
252
+
253
+ // Auto-register on page load if site data is available
254
+ document.addEventListener('DOMContentLoaded', async function() {
255
+ // Check if this is a Tunecamp site (has site metadata)
256
+ const siteTitle = document.querySelector('meta[name="tunecamp-title"]')?.content ||
257
+ document.querySelector('.site-title a')?.textContent ||
258
+ document.title;
259
+
260
+ const siteDescription = document.querySelector('meta[name="description"]')?.content || '';
261
+ const artistName = document.querySelector('meta[name="tunecamp-artist"]')?.content ||
262
+ document.querySelector('.release-artist')?.textContent?.replace('by ', '') || '';
263
+
264
+ // Get cover image if available
265
+ const coverImage = document.querySelector('meta[property="og:image"]')?.content ||
266
+ document.querySelector('.release-cover-large img')?.src ||
267
+ document.querySelector('.header-image')?.src || '';
268
+
269
+ // Only register if we have a valid URL and it looks like a Tunecamp site
270
+ const isTunecampSite = document.querySelector('.site-footer a[href*="tunecamp"]') ||
271
+ document.querySelector('meta[name="generator"][content*="Tunecamp"]');
272
+
273
+ if (isTunecampSite || window.TUNECAMP_SITE) {
274
+ const registry = new TunecampCommunityRegistry();
275
+ const initialized = await registry.init();
276
+
277
+ if (initialized) {
278
+ await registry.registerSite({
279
+ url: window.location.origin + window.location.pathname.replace(/\/[^\/]*$/, '/'),
280
+ title: siteTitle,
281
+ description: siteDescription,
282
+ artistName: artistName,
283
+ coverImage: coverImage,
284
+ });
285
+ }
286
+
287
+ // Make registry available globally
288
+ window.tunecampRegistry = registry;
289
+ }
290
+ });
291
+ })();