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.
- package/.env.local +2 -0
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/LICENSE +22 -0
- package/README.md +554 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +172 -0
- package/dist/cli.js.map +1 -0
- package/dist/generator/embedGenerator.d.ts +38 -0
- package/dist/generator/embedGenerator.d.ts.map +1 -0
- package/dist/generator/embedGenerator.js +92 -0
- package/dist/generator/embedGenerator.js.map +1 -0
- package/dist/generator/feedGenerator.d.ts +50 -0
- package/dist/generator/feedGenerator.d.ts.map +1 -0
- package/dist/generator/feedGenerator.js +167 -0
- package/dist/generator/feedGenerator.js.map +1 -0
- package/dist/generator/podcastFeedGenerator.d.ts +54 -0
- package/dist/generator/podcastFeedGenerator.d.ts.map +1 -0
- package/dist/generator/podcastFeedGenerator.js +173 -0
- package/dist/generator/podcastFeedGenerator.js.map +1 -0
- package/dist/generator/proceduralCoverGenerator.d.ts +51 -0
- package/dist/generator/proceduralCoverGenerator.d.ts.map +1 -0
- package/dist/generator/proceduralCoverGenerator.js +228 -0
- package/dist/generator/proceduralCoverGenerator.js.map +1 -0
- package/dist/generator/siteGenerator.d.ts +55 -0
- package/dist/generator/siteGenerator.d.ts.map +1 -0
- package/dist/generator/siteGenerator.js +539 -0
- package/dist/generator/siteGenerator.js.map +1 -0
- package/dist/generator/templateEngine.d.ts +13 -0
- package/dist/generator/templateEngine.d.ts.map +1 -0
- package/dist/generator/templateEngine.js +146 -0
- package/dist/generator/templateEngine.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/catalogParser.d.ts +13 -0
- package/dist/parser/catalogParser.d.ts.map +1 -0
- package/dist/parser/catalogParser.js +120 -0
- package/dist/parser/catalogParser.js.map +1 -0
- package/dist/tools/generate-codes.d.ts +14 -0
- package/dist/tools/generate-codes.d.ts.map +1 -0
- package/dist/tools/generate-codes.js +274 -0
- package/dist/tools/generate-codes.js.map +1 -0
- package/dist/tools/generate-sea-pair.d.ts +14 -0
- package/dist/tools/generate-sea-pair.d.ts.map +1 -0
- package/dist/tools/generate-sea-pair.js +111 -0
- package/dist/tools/generate-sea-pair.js.map +1 -0
- package/dist/types/index.d.ts +117 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/audioUtils.d.ts +9 -0
- package/dist/utils/audioUtils.d.ts.map +1 -0
- package/dist/utils/audioUtils.js +67 -0
- package/dist/utils/audioUtils.js.map +1 -0
- package/dist/utils/configUtils.d.ts +11 -0
- package/dist/utils/configUtils.d.ts.map +1 -0
- package/dist/utils/configUtils.js +50 -0
- package/dist/utils/configUtils.js.map +1 -0
- package/dist/utils/fileUtils.d.ts +14 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +73 -0
- package/dist/utils/fileUtils.js.map +1 -0
- package/examples/artist-free/README.md +36 -0
- package/examples/artist-paycurtain/README.md +49 -0
- package/examples/label/README.md +33 -0
- package/gundb-keypair.json +8 -0
- package/logo.svg +30 -0
- package/package-lock.json +1176 -0
- package/package.json +42 -0
- package/public/assets/community-registry.js +291 -0
- package/public/assets/download-stats.js +263 -0
- package/public/assets/player.js +219 -0
- package/public/assets/style.css +1170 -0
- package/public/assets/theme-widget.js +353 -0
- package/public/assets/unlock-codes.js +225 -0
- package/public/atom.xml +22 -0
- package/public/catalog.m3u +3 -0
- package/public/feed.xml +22 -0
- package/public/image.png +0 -0
- package/public/index.html +249 -0
- package/public/logo.svg +30 -0
- package/public/releases/chirichetto/Homologo - Chirichetto.wav +0 -0
- package/public/releases/chirichetto/cover.png +0 -0
- package/public/releases/chirichetto/embed-code.txt +16 -0
- package/public/releases/chirichetto/embed-compact.txt +8 -0
- package/public/releases/chirichetto/embed.html +39 -0
- package/public/releases/chirichetto/index.html +389 -0
- package/public/releases/chirichetto/playlist.m3u +3 -0
- package/templates/dark/assets/community-registry.js +291 -0
- package/templates/dark/assets/download-stats.js +263 -0
- package/templates/dark/assets/player.js +219 -0
- package/templates/dark/assets/style.css +740 -0
- package/templates/dark/index.hbs +73 -0
- package/templates/dark/layout.hbs +84 -0
- package/templates/dark/release.hbs +212 -0
- package/templates/default/assets/community-registry.js +291 -0
- package/templates/default/assets/download-stats.js +263 -0
- package/templates/default/assets/player.js +219 -0
- package/templates/default/assets/style.css +1170 -0
- package/templates/default/assets/theme-widget.js +353 -0
- package/templates/default/assets/unlock-codes.js +225 -0
- package/templates/default/index.hbs +188 -0
- package/templates/default/layout.hbs +117 -0
- package/templates/default/release.hbs +553 -0
- package/templates/minimal/assets/community-registry.js +291 -0
- package/templates/minimal/assets/download-stats.js +263 -0
- package/templates/minimal/assets/player.js +219 -0
- package/templates/minimal/assets/style.css +796 -0
- package/templates/minimal/index.hbs +73 -0
- package/templates/minimal/layout.hbs +84 -0
- package/templates/minimal/release.hbs +212 -0
- package/templates/retro/assets/community-registry.js +291 -0
- package/templates/retro/assets/download-stats.js +263 -0
- package/templates/retro/assets/player.js +219 -0
- package/templates/retro/assets/style.css +872 -0
- package/templates/retro/index.hbs +73 -0
- package/templates/retro/layout.hbs +84 -0
- package/templates/retro/release.hbs +212 -0
- package/templates/translucent/assets/community-registry.js +291 -0
- package/templates/translucent/assets/download-stats.js +263 -0
- package/templates/translucent/assets/player.js +219 -0
- package/templates/translucent/assets/style.css +1352 -0
- package/templates/translucent/index.hbs +73 -0
- package/templates/translucent/layout.hbs +84 -0
- package/templates/translucent/release.hbs +212 -0
- package/website/community.html +492 -0
- package/website/index.html +195 -0
- package/website/styles.css +396 -0
- package/website/tunecamp.svg +30 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="it" data-theme="dark">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Chirichetto - Homologo</title>
|
|
8
|
+
<meta name="description" content="Faccio musica perché mi piace, punto. Non mi interessa metterla in scatole o darle etichette. A volte suona in un modo, a volte in un altro. Dipende da come mi sento quel giorno. Se vi va, date un'occhiata. Se vi piace, bene. Se no, nessun problema. Non prometto niente di speciale. È solo la mia roba, per quel che vale">
|
|
9
|
+
<!-- Tunecamp Metadata (for community registry) -->
|
|
10
|
+
<meta name="generator" content="Tunecamp 0.1.0">
|
|
11
|
+
<meta name="tunecamp-title" content="Homologo">
|
|
12
|
+
<meta name="tunecamp-artist" content="Homologo">
|
|
13
|
+
<!-- RSS/Atom Feeds -->
|
|
14
|
+
<link rel="alternate" type="application/rss+xml" title="Homologo RSS Feed" href="../../feed.xml">
|
|
15
|
+
<link rel="alternate" type="application/atom+xml" title="Homologo Atom Feed" href="../../atom.xml">
|
|
16
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;600;700&display=swap">
|
|
17
|
+
<link rel="stylesheet" href="../../assets/style.css">
|
|
18
|
+
<style>
|
|
19
|
+
:root {
|
|
20
|
+
--custom-font-family: 'Google Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
|
21
|
+
}
|
|
22
|
+
body {
|
|
23
|
+
font-family: var(--custom-font-family);
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
26
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
27
|
+
<script>
|
|
28
|
+
// Initialize theme from localStorage or system preference
|
|
29
|
+
(function () {
|
|
30
|
+
const saved = localStorage.getItem('theme');
|
|
31
|
+
if (saved) {
|
|
32
|
+
document.documentElement.setAttribute('data-theme', saved);
|
|
33
|
+
} else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
|
34
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
</script>
|
|
38
|
+
</head>
|
|
39
|
+
|
|
40
|
+
<body>
|
|
41
|
+
<header class="site-header">
|
|
42
|
+
<div class="container">
|
|
43
|
+
<div class="header-image-wrapper">
|
|
44
|
+
<div class="site-header-image">
|
|
45
|
+
<a href="../../index.html">
|
|
46
|
+
<img src="../../image.png" alt="Homologo" class="header-image">
|
|
47
|
+
</a>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</header>
|
|
52
|
+
|
|
53
|
+
<main class="site-main">
|
|
54
|
+
<div class="container">
|
|
55
|
+
<nav class="breadcrumb">
|
|
56
|
+
<a href="../../index.html">← Back to catalog</a>
|
|
57
|
+
</nav>
|
|
58
|
+
|
|
59
|
+
<!-- Share & Embed Section -->
|
|
60
|
+
<section class="release-share-section">
|
|
61
|
+
<div class="share-actions">
|
|
62
|
+
<h3>Share & Embed</h3>
|
|
63
|
+
<div class="share-buttons">
|
|
64
|
+
<a href="../../index.html" class="share-btn" title="View Catalog">
|
|
65
|
+
<i class="fas fa-list"></i> Catalog
|
|
66
|
+
</a>
|
|
67
|
+
<a href="../../feed.xml" class="share-btn" title="RSS Feed" target="_blank">
|
|
68
|
+
<i class="fas fa-rss"></i> RSS Feed
|
|
69
|
+
</a>
|
|
70
|
+
<a href="../../atom.xml" class="share-btn" title="Atom Feed" target="_blank">
|
|
71
|
+
<i class="fas fa-rss-square"></i> Atom Feed
|
|
72
|
+
</a>
|
|
73
|
+
<button class="share-btn" onclick="copyShareLink()" title="Copy Share Link">
|
|
74
|
+
<i class="fas fa-link"></i> Copy Link
|
|
75
|
+
</button>
|
|
76
|
+
<button class="share-btn" onclick="showEmbedCode()" title="Show Embed Code">
|
|
77
|
+
<i class="fas fa-code"></i> Embed Code
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
82
|
+
|
|
83
|
+
<!-- Embed Code Modal -->
|
|
84
|
+
<div id="embedModal" class="embed-modal" style="display: none;">
|
|
85
|
+
<div class="embed-modal-content">
|
|
86
|
+
<div class="embed-modal-header">
|
|
87
|
+
<h3>Embed Code</h3>
|
|
88
|
+
<button class="embed-modal-close" onclick="closeEmbedModal()">×</button>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="embed-modal-body">
|
|
91
|
+
<div class="embed-tabs">
|
|
92
|
+
<button class="embed-tab active" onclick="switchEmbedTab('full')">Full Embed</button>
|
|
93
|
+
<button class="embed-tab" onclick="switchEmbedTab('compact')">Compact</button>
|
|
94
|
+
</div>
|
|
95
|
+
<div id="embedCodeContent" class="embed-code-content">
|
|
96
|
+
<textarea id="embedCodeText" readonly class="embed-code-text"></textarea>
|
|
97
|
+
<button class="btn btn-primary" onclick="copyEmbedCode()">
|
|
98
|
+
<i class="fas fa-copy"></i> Copy Code
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<article class="release-detail">
|
|
106
|
+
<header class="release-header">
|
|
107
|
+
<div class="release-cover-large">
|
|
108
|
+
<img src="./cover.png" alt="Chirichetto">
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="release-metadata">
|
|
112
|
+
<h1>Chirichetto</h1>
|
|
113
|
+
|
|
114
|
+
<p class="release-artist">by Homologo</p>
|
|
115
|
+
|
|
116
|
+
<p class="release-date">Released January 13, 2026</p>
|
|
117
|
+
|
|
118
|
+
<p class="release-downloads" id="releaseDownloads" style="display: none;">
|
|
119
|
+
<i class="fas fa-download"></i> <span id="downloadCount">0</span> downloads
|
|
120
|
+
</p>
|
|
121
|
+
|
|
122
|
+
<div class="release-genres">
|
|
123
|
+
<span class="genre-tag">Electronic</span>
|
|
124
|
+
<span class="genre-tag">Pop</span>
|
|
125
|
+
<span class="genre-tag">Club</span>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<p class="release-description">Ci provo io con te ma non so se mi dirai di si!?</p>
|
|
129
|
+
|
|
130
|
+
<div class="release-credits">
|
|
131
|
+
<h3>Credits</h3>
|
|
132
|
+
<ul>
|
|
133
|
+
<li><strong>Produced by:</strong> Francesco Bruno</li>
|
|
134
|
+
</ul>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
<div class="release-download-actions">
|
|
139
|
+
<button class="btn btn-primary" onclick="downloadAll()">
|
|
140
|
+
<i class="fas fa-download"></i> Download All (Free)
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</header>
|
|
145
|
+
|
|
146
|
+
<section class="tracklist">
|
|
147
|
+
<h2>Tracks</h2>
|
|
148
|
+
|
|
149
|
+
<div class="audio-player" id="audioPlayer">
|
|
150
|
+
<div class="player-track-info">
|
|
151
|
+
<div class="player-cover">
|
|
152
|
+
<img src="cover.png" alt="Chirichetto" id="playerCover">
|
|
153
|
+
</div>
|
|
154
|
+
<div class="player-details">
|
|
155
|
+
<div class="player-title" id="playerTitle">Select a track</div>
|
|
156
|
+
<div class="player-artist" id="playerArtist">Homologo</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="player-controls">
|
|
161
|
+
<button class="player-btn" id="prevBtn"><i class="fas fa-step-backward"></i></button>
|
|
162
|
+
<button class="player-btn player-btn-play" id="playBtn">
|
|
163
|
+
<i class="fas fa-play"></i>
|
|
164
|
+
</button>
|
|
165
|
+
<button class="player-btn" id="nextBtn"><i class="fas fa-step-forward"></i></button>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div class="player-progress">
|
|
169
|
+
<span class="player-time" id="currentTime">0:00</span>
|
|
170
|
+
<input type="range" class="progress-bar" id="progressBar" min="0" max="100" value="0">
|
|
171
|
+
<span class="player-time" id="duration">0:00</span>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div class="player-volume">
|
|
175
|
+
<i class="fas fa-volume-up"></i>
|
|
176
|
+
<input type="range" class="volume-bar" id="volumeBar" min="0" max="100" value="80">
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<ol class="track-list">
|
|
181
|
+
<li class="track-item" data-src="Homologo - Chirichetto.wav" data-index="0">
|
|
182
|
+
<div class="track-number">0</div>
|
|
183
|
+
<div class="track-info">
|
|
184
|
+
<div class="track-title">Homologo - Chirichetto</div>
|
|
185
|
+
<div class="track-meta">
|
|
186
|
+
WAV
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="track-actions">
|
|
190
|
+
<button class="track-play-btn" onclick="playTrack(0)">
|
|
191
|
+
<i class="fas fa-play"></i>
|
|
192
|
+
</button>
|
|
193
|
+
<a href="Homologo - Chirichetto.wav" download class="track-download-btn">
|
|
194
|
+
<i class="fas fa-download"></i>
|
|
195
|
+
</a>
|
|
196
|
+
</div>
|
|
197
|
+
</li>
|
|
198
|
+
</ol>
|
|
199
|
+
</section>
|
|
200
|
+
|
|
201
|
+
</article>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<script>
|
|
205
|
+
// Share & Embed functionality
|
|
206
|
+
function copyShareLink() {
|
|
207
|
+
const url = window.location.href;
|
|
208
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
209
|
+
alert('Link copied to clipboard!');
|
|
210
|
+
}).catch(() => {
|
|
211
|
+
// Fallback for older browsers
|
|
212
|
+
const textarea = document.createElement('textarea');
|
|
213
|
+
textarea.value = url;
|
|
214
|
+
document.body.appendChild(textarea);
|
|
215
|
+
textarea.select();
|
|
216
|
+
document.execCommand('copy');
|
|
217
|
+
document.body.removeChild(textarea);
|
|
218
|
+
alert('Link copied to clipboard!');
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function showEmbedCode() {
|
|
223
|
+
const modal = document.getElementById('embedModal');
|
|
224
|
+
const embedCodeText = document.getElementById('embedCodeText');
|
|
225
|
+
|
|
226
|
+
// Load full embed code by default
|
|
227
|
+
fetch('./embed-code.txt')
|
|
228
|
+
.then(response => response.text())
|
|
229
|
+
.then(text => {
|
|
230
|
+
embedCodeText.value = text.trim();
|
|
231
|
+
modal.style.display = 'flex';
|
|
232
|
+
})
|
|
233
|
+
.catch(err => {
|
|
234
|
+
console.error('Error loading embed code:', err);
|
|
235
|
+
alert('Error loading embed code');
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function switchEmbedTab(type) {
|
|
240
|
+
const tabs = document.querySelectorAll('.embed-tab');
|
|
241
|
+
tabs.forEach(tab => tab.classList.remove('active'));
|
|
242
|
+
event.target.classList.add('active');
|
|
243
|
+
|
|
244
|
+
const embedCodeText = document.getElementById('embedCodeText');
|
|
245
|
+
const file = type === 'full' ? 'embed-code.txt' : 'embed-compact.txt';
|
|
246
|
+
|
|
247
|
+
fetch('./' + file)
|
|
248
|
+
.then(response => response.text())
|
|
249
|
+
.then(text => {
|
|
250
|
+
embedCodeText.value = text.trim();
|
|
251
|
+
})
|
|
252
|
+
.catch(err => {
|
|
253
|
+
console.error('Error loading embed code:', err);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function copyEmbedCode() {
|
|
258
|
+
const embedCodeText = document.getElementById('embedCodeText');
|
|
259
|
+
embedCodeText.select();
|
|
260
|
+
document.execCommand('copy');
|
|
261
|
+
alert('Embed code copied to clipboard!');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function closeEmbedModal() {
|
|
265
|
+
document.getElementById('embedModal').style.display = 'none';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Close modal when clicking outside
|
|
269
|
+
window.onclick = function(event) {
|
|
270
|
+
const modal = document.getElementById('embedModal');
|
|
271
|
+
if (event.target === modal) {
|
|
272
|
+
modal.style.display = 'none';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
<script>
|
|
278
|
+
// Initialize player with tracks
|
|
279
|
+
window.tracks = [
|
|
280
|
+
{
|
|
281
|
+
url: 'Homologo - Chirichetto.wav',
|
|
282
|
+
title: 'Homologo - Chirichetto',
|
|
283
|
+
artist: 'Homologo',
|
|
284
|
+
duration: 0
|
|
285
|
+
}
|
|
286
|
+
];
|
|
287
|
+
</script>
|
|
288
|
+
|
|
289
|
+
<!-- Download Stats (GunDB) -->
|
|
290
|
+
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
|
291
|
+
<script src="../../assets/download-stats.js"></script>
|
|
292
|
+
<script>
|
|
293
|
+
(function() {
|
|
294
|
+
const releaseSlug = 'chirichetto';
|
|
295
|
+
let downloadStats;
|
|
296
|
+
|
|
297
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
298
|
+
// Initialize download stats
|
|
299
|
+
downloadStats = new TunecampDownloadStats();
|
|
300
|
+
window.downloadStats = downloadStats;
|
|
301
|
+
|
|
302
|
+
// Load and display current download count
|
|
303
|
+
loadDownloadCount();
|
|
304
|
+
|
|
305
|
+
// Subscribe to real-time updates
|
|
306
|
+
downloadStats.subscribeToDownloadCount(releaseSlug, updateDownloadDisplay);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
async function loadDownloadCount() {
|
|
310
|
+
try {
|
|
311
|
+
const count = await downloadStats.getDownloadCount(releaseSlug);
|
|
312
|
+
updateDownloadDisplay(count);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.warn('Could not load download count:', e);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function updateDownloadDisplay(count) {
|
|
319
|
+
const el = document.getElementById('releaseDownloads');
|
|
320
|
+
const countEl = document.getElementById('downloadCount');
|
|
321
|
+
if (el && countEl) {
|
|
322
|
+
countEl.textContent = downloadStats.formatCount(count);
|
|
323
|
+
el.style.display = 'block';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Register hook for downloadAll tracking
|
|
328
|
+
window.onDownloadAll = async function() {
|
|
329
|
+
if (downloadStats) {
|
|
330
|
+
try {
|
|
331
|
+
await downloadStats.incrementDownloadCount(releaseSlug);
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.warn('Could not track download:', e);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Track individual track downloads
|
|
339
|
+
document.querySelectorAll('.track-download-btn').forEach((btn, index) => {
|
|
340
|
+
btn.addEventListener('click', async function(e) {
|
|
341
|
+
if (downloadStats && window.tracks && window.tracks[index]) {
|
|
342
|
+
try {
|
|
343
|
+
const trackId = window.tracks[index].title.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
|
|
344
|
+
await downloadStats.incrementTrackDownloadCount(releaseSlug, trackId);
|
|
345
|
+
await downloadStats.incrementDownloadCount(releaseSlug);
|
|
346
|
+
} catch (e) {
|
|
347
|
+
console.warn('Could not track track download:', e);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
})();
|
|
353
|
+
</script>
|
|
354
|
+
</main>
|
|
355
|
+
|
|
356
|
+
<footer class="site-footer">
|
|
357
|
+
<div class="container">
|
|
358
|
+
<p>
|
|
359
|
+
© Homologo -
|
|
360
|
+
Powered by <a href="https://github.com/scobru/tunecamp" target="_blank">Tunecamp</a>
|
|
361
|
+
</p>
|
|
362
|
+
<div class="social-links">
|
|
363
|
+
<a href="https://homologomusic.vercel.app" target="_blank" rel="noopener" class="social-link">
|
|
364
|
+
<i class="fab fa-website"></i>
|
|
365
|
+
</a>
|
|
366
|
+
<a href="https://homologomusic.bandcamp.com" target="_blank" rel="noopener" class="social-link">
|
|
367
|
+
<i class="fab fa-bandcamp"></i>
|
|
368
|
+
</a>
|
|
369
|
+
<a href="https://open.spotify.com/intl-it/artist/10WDitiwNCQp8bHO4guJvj?si=HHCAAsZqTlKRVCpHl6tS4Q." target="_blank" rel="noopener" class="social-link">
|
|
370
|
+
<i class="fab fa-spotify"></i>
|
|
371
|
+
</a>
|
|
372
|
+
<a href="https://www.instagram.com/homologomusic" target="_blank" rel="noopener" class="social-link">
|
|
373
|
+
<i class="fab fa-instagram"></i>
|
|
374
|
+
</a>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</footer>
|
|
378
|
+
|
|
379
|
+
<script src="../../assets/player.js"></script>
|
|
380
|
+
|
|
381
|
+
<!-- Interactive Theme Widget -->
|
|
382
|
+
<script src="../../assets/theme-widget.js"></script>
|
|
383
|
+
|
|
384
|
+
<!-- Tunecamp Community Registry (auto-registers site) -->
|
|
385
|
+
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
|
386
|
+
<script src="../../assets/community-registry.js"></script>
|
|
387
|
+
</body>
|
|
388
|
+
|
|
389
|
+
</html>
|
|
@@ -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
|
+
})();
|