@rmdes/indiekit-endpoint-lastfm 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/README.md +74 -0
- package/includes/@indiekit-endpoint-lastfm-now-playing.njk +90 -0
- package/includes/@indiekit-endpoint-lastfm-stats.njk +75 -0
- package/includes/@indiekit-endpoint-lastfm-widget.njk +12 -0
- package/index.js +110 -0
- package/lib/controllers/dashboard.js +140 -0
- package/lib/controllers/loved.js +52 -0
- package/lib/controllers/now-playing.js +57 -0
- package/lib/controllers/scrobbles.js +52 -0
- package/lib/controllers/stats.js +89 -0
- package/lib/lastfm-client.js +258 -0
- package/lib/stats.js +308 -0
- package/lib/sync.js +226 -0
- package/lib/utils.js +364 -0
- package/locales/en.json +37 -0
- package/package.json +54 -0
- package/views/lastfm.njk +295 -0
package/views/lastfm.njk
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<style>
|
|
5
|
+
/* Inline styles to ensure proper rendering */
|
|
6
|
+
.lfm-section { margin-bottom: 2rem; }
|
|
7
|
+
.lfm-section h2 { margin-bottom: 1rem; }
|
|
8
|
+
|
|
9
|
+
/* Stats grid */
|
|
10
|
+
.lfm-stats-grid {
|
|
11
|
+
display: grid;
|
|
12
|
+
grid-template-columns: repeat(3, 1fr);
|
|
13
|
+
gap: 1rem;
|
|
14
|
+
margin-bottom: 1rem;
|
|
15
|
+
}
|
|
16
|
+
.lfm-stat {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
align-items: center;
|
|
20
|
+
padding: 1rem;
|
|
21
|
+
background: var(--color-offset, #f5f5f5);
|
|
22
|
+
border-radius: 0.5rem;
|
|
23
|
+
text-align: center;
|
|
24
|
+
}
|
|
25
|
+
.lfm-stat-value {
|
|
26
|
+
font-size: 1.5rem;
|
|
27
|
+
font-weight: 700;
|
|
28
|
+
color: var(--color-accent, #d51007);
|
|
29
|
+
}
|
|
30
|
+
.lfm-stat-label {
|
|
31
|
+
font-size: 0.75rem;
|
|
32
|
+
color: var(--color-text-secondary, #666);
|
|
33
|
+
text-transform: uppercase;
|
|
34
|
+
letter-spacing: 0.05em;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Featured track */
|
|
38
|
+
.lfm-featured {
|
|
39
|
+
display: flex;
|
|
40
|
+
gap: 1rem;
|
|
41
|
+
padding: 1rem;
|
|
42
|
+
background: var(--color-offset, #f5f5f5);
|
|
43
|
+
border-radius: 0.5rem;
|
|
44
|
+
align-items: flex-start;
|
|
45
|
+
}
|
|
46
|
+
.lfm-featured img {
|
|
47
|
+
width: 80px;
|
|
48
|
+
height: 80px;
|
|
49
|
+
object-fit: cover;
|
|
50
|
+
border-radius: 0.25rem;
|
|
51
|
+
flex-shrink: 0;
|
|
52
|
+
}
|
|
53
|
+
.lfm-featured-info { flex: 1; }
|
|
54
|
+
.lfm-featured-title {
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
text-decoration: none;
|
|
57
|
+
color: inherit;
|
|
58
|
+
display: block;
|
|
59
|
+
}
|
|
60
|
+
.lfm-featured-title:hover { text-decoration: underline; }
|
|
61
|
+
.lfm-featured-album {
|
|
62
|
+
margin: 0.25rem 0;
|
|
63
|
+
color: var(--color-text-secondary, #666);
|
|
64
|
+
font-size: 0.875rem;
|
|
65
|
+
}
|
|
66
|
+
.lfm-meta {
|
|
67
|
+
display: flex;
|
|
68
|
+
gap: 0.5rem;
|
|
69
|
+
color: var(--color-text-secondary, #666);
|
|
70
|
+
font-size: 0.75rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Now playing animation */
|
|
74
|
+
.lfm-playing-status {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 0.5rem;
|
|
78
|
+
color: #d51007;
|
|
79
|
+
}
|
|
80
|
+
.lfm-bars {
|
|
81
|
+
display: flex;
|
|
82
|
+
gap: 2px;
|
|
83
|
+
align-items: flex-end;
|
|
84
|
+
height: 16px;
|
|
85
|
+
}
|
|
86
|
+
.lfm-bars span {
|
|
87
|
+
width: 3px;
|
|
88
|
+
background: currentColor;
|
|
89
|
+
animation: lfm-bar 0.5s infinite ease-in-out alternate;
|
|
90
|
+
}
|
|
91
|
+
.lfm-bars span:nth-child(2) { animation-delay: 0.2s; }
|
|
92
|
+
.lfm-bars span:nth-child(3) { animation-delay: 0.4s; }
|
|
93
|
+
@keyframes lfm-bar {
|
|
94
|
+
from { height: 4px; }
|
|
95
|
+
to { height: 16px; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Loved indicator */
|
|
99
|
+
.lfm-loved {
|
|
100
|
+
color: #d51007;
|
|
101
|
+
font-size: 0.875rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* List */
|
|
105
|
+
.lfm-list {
|
|
106
|
+
list-style: none;
|
|
107
|
+
padding: 0;
|
|
108
|
+
margin: 0 0 1rem 0;
|
|
109
|
+
}
|
|
110
|
+
.lfm-list-item {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 0.75rem;
|
|
114
|
+
padding: 0.75rem 0;
|
|
115
|
+
border-bottom: 1px solid var(--color-border, #eee);
|
|
116
|
+
}
|
|
117
|
+
.lfm-list-item:last-child { border-bottom: none; }
|
|
118
|
+
.lfm-list-item img {
|
|
119
|
+
width: 48px;
|
|
120
|
+
height: 48px;
|
|
121
|
+
object-fit: cover;
|
|
122
|
+
border-radius: 0.25rem;
|
|
123
|
+
flex-shrink: 0;
|
|
124
|
+
}
|
|
125
|
+
.lfm-list-item-placeholder {
|
|
126
|
+
width: 48px;
|
|
127
|
+
height: 48px;
|
|
128
|
+
background: var(--color-border, #ddd);
|
|
129
|
+
border-radius: 0.25rem;
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
}
|
|
132
|
+
.lfm-list-info {
|
|
133
|
+
flex: 1;
|
|
134
|
+
min-width: 0;
|
|
135
|
+
}
|
|
136
|
+
.lfm-list-title {
|
|
137
|
+
display: block;
|
|
138
|
+
font-weight: 500;
|
|
139
|
+
text-decoration: none;
|
|
140
|
+
color: inherit;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
text-overflow: ellipsis;
|
|
144
|
+
}
|
|
145
|
+
.lfm-list-title:hover { text-decoration: underline; }
|
|
146
|
+
|
|
147
|
+
/* Public link banner */
|
|
148
|
+
.lfm-public-link {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: space-between;
|
|
152
|
+
padding: 1rem;
|
|
153
|
+
background: var(--color-offset, #f5f5f5);
|
|
154
|
+
border-radius: 0.5rem;
|
|
155
|
+
margin-top: 2rem;
|
|
156
|
+
}
|
|
157
|
+
.lfm-public-link p {
|
|
158
|
+
margin: 0;
|
|
159
|
+
color: var(--color-text-secondary, #666);
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
162
|
+
|
|
163
|
+
{% if error %}
|
|
164
|
+
{{ prose({ text: error.message }) }}
|
|
165
|
+
{% else %}
|
|
166
|
+
{# Now Playing / Recently Played #}
|
|
167
|
+
{% if nowPlaying %}
|
|
168
|
+
<section class="lfm-section">
|
|
169
|
+
<h2>
|
|
170
|
+
{% if nowPlaying.status == "now-playing" %}
|
|
171
|
+
<span class="lfm-playing-status">
|
|
172
|
+
<span class="lfm-bars">
|
|
173
|
+
<span></span><span></span><span></span>
|
|
174
|
+
</span>
|
|
175
|
+
{{ __("lastfm.nowPlaying") }}
|
|
176
|
+
</span>
|
|
177
|
+
{% else %}
|
|
178
|
+
{{ __("lastfm.recentlyPlayed") }}
|
|
179
|
+
{% endif %}
|
|
180
|
+
</h2>
|
|
181
|
+
<article class="lfm-featured">
|
|
182
|
+
{% if nowPlaying.coverUrl %}
|
|
183
|
+
<img src="{{ nowPlaying.coverUrl }}" alt="" loading="lazy">
|
|
184
|
+
{% endif %}
|
|
185
|
+
<div class="lfm-featured-info">
|
|
186
|
+
<a href="{{ nowPlaying.trackUrl }}" class="lfm-featured-title" target="_blank" rel="noopener">
|
|
187
|
+
{{ nowPlaying.artist }} - {{ nowPlaying.track }}
|
|
188
|
+
</a>
|
|
189
|
+
{% if nowPlaying.album %}
|
|
190
|
+
<p class="lfm-featured-album">{{ nowPlaying.album }}</p>
|
|
191
|
+
{% endif %}
|
|
192
|
+
<small class="lfm-meta">
|
|
193
|
+
{% if nowPlaying.loved %}<span class="lfm-loved">♥</span>{% endif %}
|
|
194
|
+
<span>{{ nowPlaying.relativeTime }}</span>
|
|
195
|
+
</small>
|
|
196
|
+
</div>
|
|
197
|
+
</article>
|
|
198
|
+
</section>
|
|
199
|
+
{% endif %}
|
|
200
|
+
|
|
201
|
+
{# Quick Stats Summary #}
|
|
202
|
+
{% if hasStats %}
|
|
203
|
+
<section class="lfm-section">
|
|
204
|
+
<h2>{{ __("lastfm.stats") }}</h2>
|
|
205
|
+
<div class="lfm-stats-grid">
|
|
206
|
+
<div class="lfm-stat">
|
|
207
|
+
<span class="lfm-stat-value">{{ totalPlays }}</span>
|
|
208
|
+
<span class="lfm-stat-label">{{ __("lastfm.plays") }}</span>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="lfm-stat">
|
|
211
|
+
<span class="lfm-stat-value">{{ uniqueTracks }}</span>
|
|
212
|
+
<span class="lfm-stat-label">{{ __("lastfm.tracks") }}</span>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="lfm-stat">
|
|
215
|
+
<span class="lfm-stat-value">{{ uniqueArtists }}</span>
|
|
216
|
+
<span class="lfm-stat-label">{{ __("lastfm.artists") }}</span>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</section>
|
|
220
|
+
{% endif %}
|
|
221
|
+
|
|
222
|
+
{# Recent Scrobbles #}
|
|
223
|
+
<section class="lfm-section">
|
|
224
|
+
<h2>{{ __("lastfm.scrobbles") }}</h2>
|
|
225
|
+
{% if scrobbles and scrobbles.length > 0 %}
|
|
226
|
+
<ul class="lfm-list">
|
|
227
|
+
{% for scrobble in scrobbles %}
|
|
228
|
+
<li class="lfm-list-item">
|
|
229
|
+
{% if scrobble.coverUrl %}
|
|
230
|
+
<img src="{{ scrobble.coverUrl }}" alt="" loading="lazy">
|
|
231
|
+
{% else %}
|
|
232
|
+
<div class="lfm-list-item-placeholder"></div>
|
|
233
|
+
{% endif %}
|
|
234
|
+
<div class="lfm-list-info">
|
|
235
|
+
<a href="{{ scrobble.trackUrl }}" class="lfm-list-title" target="_blank" rel="noopener">
|
|
236
|
+
{{ scrobble.artist }} - {{ scrobble.track }}
|
|
237
|
+
</a>
|
|
238
|
+
<small class="lfm-meta">
|
|
239
|
+
{% if scrobble.loved %}<span class="lfm-loved">♥</span>{% endif %}
|
|
240
|
+
{{ scrobble.relativeTime }}
|
|
241
|
+
</small>
|
|
242
|
+
</div>
|
|
243
|
+
{% if scrobble.status == "now-playing" %}
|
|
244
|
+
{{ badge({ color: "red", text: __("lastfm.nowPlaying") }) }}
|
|
245
|
+
{% elif scrobble.status == "recently-played" %}
|
|
246
|
+
{{ badge({ color: "blue", text: __("lastfm.recentlyPlayed") }) }}
|
|
247
|
+
{% endif %}
|
|
248
|
+
</li>
|
|
249
|
+
{% endfor %}
|
|
250
|
+
</ul>
|
|
251
|
+
{% else %}
|
|
252
|
+
{{ prose({ text: __("lastfm.noRecentPlays") }) }}
|
|
253
|
+
{% endif %}
|
|
254
|
+
</section>
|
|
255
|
+
|
|
256
|
+
{# Loved Tracks #}
|
|
257
|
+
<section class="lfm-section">
|
|
258
|
+
<h2>{{ __("lastfm.loved") }}</h2>
|
|
259
|
+
{% if lovedTracks and lovedTracks.length > 0 %}
|
|
260
|
+
<ul class="lfm-list">
|
|
261
|
+
{% for track in lovedTracks %}
|
|
262
|
+
<li class="lfm-list-item">
|
|
263
|
+
{% if track.coverUrl %}
|
|
264
|
+
<img src="{{ track.coverUrl }}" alt="" loading="lazy">
|
|
265
|
+
{% else %}
|
|
266
|
+
<div class="lfm-list-item-placeholder"></div>
|
|
267
|
+
{% endif %}
|
|
268
|
+
<div class="lfm-list-info">
|
|
269
|
+
<a href="{{ track.trackUrl }}" class="lfm-list-title" target="_blank" rel="noopener">
|
|
270
|
+
{{ track.artist }} - {{ track.track }}
|
|
271
|
+
</a>
|
|
272
|
+
<small class="lfm-meta">
|
|
273
|
+
<span class="lfm-loved">♥</span>
|
|
274
|
+
{{ track.relativeTime }}
|
|
275
|
+
</small>
|
|
276
|
+
</div>
|
|
277
|
+
</li>
|
|
278
|
+
{% endfor %}
|
|
279
|
+
</ul>
|
|
280
|
+
{% else %}
|
|
281
|
+
{{ prose({ text: __("lastfm.noLoved") }) }}
|
|
282
|
+
{% endif %}
|
|
283
|
+
</section>
|
|
284
|
+
|
|
285
|
+
{# Link to public page #}
|
|
286
|
+
<div class="lfm-public-link">
|
|
287
|
+
<p>{{ __("lastfm.widget.description") }}</p>
|
|
288
|
+
{{ button({
|
|
289
|
+
href: publicUrl,
|
|
290
|
+
text: __("lastfm.widget.view"),
|
|
291
|
+
target: "_blank"
|
|
292
|
+
}) }}
|
|
293
|
+
</div>
|
|
294
|
+
{% endif %}
|
|
295
|
+
{% endblock %}
|