@rmdes/indiekit-endpoint-funkwhale 1.0.3 → 1.0.5

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/index.js CHANGED
@@ -45,10 +45,6 @@ export default class FunkwhaleEndpoint {
45
45
  return path.join(__dirname, "locales");
46
46
  }
47
47
 
48
- get styles() {
49
- return path.join(__dirname, "assets", "styles.css");
50
- }
51
-
52
48
  get navigationItems() {
53
49
  return {
54
50
  href: this.options.mountPath,
@@ -68,17 +64,12 @@ export default class FunkwhaleEndpoint {
68
64
 
69
65
  /**
70
66
  * Protected routes (require authentication)
71
- * HTML pages for admin dashboard
67
+ * Admin dashboard only - detailed views are on the public frontend
72
68
  */
73
69
  get routes() {
74
- // Dashboard
70
+ // Dashboard overview
75
71
  protectedRouter.get("/", dashboardController.get);
76
72
 
77
- // Individual sections (HTML pages)
78
- protectedRouter.get("/listenings", listeningsController.get);
79
- protectedRouter.get("/favorites", favoritesController.get);
80
- protectedRouter.get("/stats", statsController.get);
81
-
82
73
  // Manual sync trigger
83
74
  protectedRouter.post("/sync", dashboardController.sync);
84
75
 
@@ -87,13 +78,10 @@ export default class FunkwhaleEndpoint {
87
78
 
88
79
  /**
89
80
  * Public routes (no authentication required)
90
- * JSON API endpoints for Eleventy widgets
81
+ * JSON API endpoints for Eleventy frontend
91
82
  */
92
83
  get routesPublic() {
93
- // Now playing widget
94
84
  publicRouter.get("/api/now-playing", nowPlayingController.api);
95
-
96
- // JSON API for widgets
97
85
  publicRouter.get("/api/listenings", listeningsController.api);
98
86
  publicRouter.get("/api/favorites", favoritesController.api);
99
87
  publicRouter.get("/api/stats", statsController.api);
@@ -112,6 +100,9 @@ export default class FunkwhaleEndpoint {
112
100
  Indiekit.config.application.funkwhaleConfig = this.options;
113
101
  Indiekit.config.application.funkwhaleEndpoint = this.mountPath;
114
102
 
103
+ // Store database getter for controller access
104
+ Indiekit.config.application.getFunkwhaleDb = () => Indiekit.database;
105
+
115
106
  // Start background sync if database is available
116
107
  if (Indiekit.config.application.mongodbUrl) {
117
108
  startSync(Indiekit, this.options);
@@ -1,6 +1,5 @@
1
1
  import { FunkwhaleClient } from "../funkwhale-client.js";
2
- import { runSync } from "../sync.js";
3
- import { getSummary } from "../stats.js";
2
+ import { runSync, getCachedStats, refreshStatsCache } from "../sync.js";
4
3
  import * as utils from "../utils.js";
5
4
 
6
5
  /**
@@ -13,7 +12,7 @@ export const dashboardController = {
13
12
  */
14
13
  async get(request, response, next) {
15
14
  try {
16
- const { funkwhaleConfig } = request.app.locals.application;
15
+ const { funkwhaleConfig, funkwhaleEndpoint } = request.app.locals.application;
17
16
 
18
17
  if (!funkwhaleConfig) {
19
18
  return response.status(500).render("funkwhale", {
@@ -64,16 +63,23 @@ export const dashboardController = {
64
63
  });
65
64
  }
66
65
 
67
- // Get stats summary from database if available
68
- let summary = null;
69
- const db = request.app.locals.database;
70
- if (db) {
71
- try {
72
- summary = await getSummary(db, "all");
73
- } catch (dbError) {
74
- console.error("[Funkwhale] DB error:", dbError.message);
66
+ // Get stats from cache (same source as public API)
67
+ // If cache is empty, try to refresh it from database
68
+ let cachedStats = getCachedStats();
69
+ if (!cachedStats) {
70
+ const getDb = request.app.locals.application.getFunkwhaleDb;
71
+ if (getDb) {
72
+ const db = getDb();
73
+ if (db) {
74
+ cachedStats = await refreshStatsCache(db, limits);
75
+ }
75
76
  }
76
77
  }
78
+ const summary = cachedStats?.summary?.all || null;
79
+
80
+ // Determine public frontend URL (strip 'api' from mount path)
81
+ // e.g., /funkwhaleapi -> /funkwhale
82
+ const publicUrl = funkwhaleEndpoint ? funkwhaleEndpoint.replace(/api$/, "") : "/funkwhale";
77
83
 
78
84
  response.render("funkwhale", {
79
85
  title: response.locals.__("funkwhale.title"),
@@ -81,6 +87,7 @@ export const dashboardController = {
81
87
  listenings: listenings.slice(0, 5),
82
88
  favorites: favorites.slice(0, 5),
83
89
  summary,
90
+ publicUrl,
84
91
  mountPath: request.baseUrl,
85
92
  });
86
93
  } catch (error) {
package/lib/sync.js CHANGED
@@ -30,6 +30,25 @@ export function setCachedStats(stats) {
30
30
  cachedStatsTime = Date.now();
31
31
  }
32
32
 
33
+ /**
34
+ * Refresh stats cache from database (for when cache is empty)
35
+ * @param {object} db - MongoDB database instance
36
+ * @param {object} limits - Limits for top lists
37
+ * @returns {Promise<object|null>} - Stats or null if failed
38
+ */
39
+ export async function refreshStatsCache(db, limits = {}) {
40
+ if (!db) return null;
41
+ try {
42
+ const stats = await getAllStats(db, limits);
43
+ setCachedStats(stats);
44
+ console.log("[Funkwhale] Stats cache refreshed on-demand");
45
+ return stats;
46
+ } catch (err) {
47
+ console.error("[Funkwhale] Failed to refresh stats cache:", err.message);
48
+ return null;
49
+ }
50
+ }
51
+
33
52
  /**
34
53
  * Start background sync process
35
54
  * @param {object} Indiekit - Indiekit instance
package/locales/en.json CHANGED
@@ -30,8 +30,8 @@
30
30
  "noConfig": "Funkwhale endpoint not configured correctly"
31
31
  },
32
32
  "widget": {
33
- "description": "View your Funkwhale listening activity",
34
- "view": "View Activity"
33
+ "description": "View full listening activity on the public page",
34
+ "view": "View Public Page"
35
35
  }
36
36
  }
37
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-funkwhale",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Funkwhale listening activity endpoint for Indiekit. Display listening history, favorites, and statistics.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -34,7 +34,6 @@
34
34
  ".": "./index.js"
35
35
  },
36
36
  "files": [
37
- "assets",
38
37
  "includes",
39
38
  "lib",
40
39
  "locales",
@@ -137,6 +137,21 @@
137
137
  text-overflow: ellipsis;
138
138
  }
139
139
  .fw-list-title:hover { text-decoration: underline; }
140
+
141
+ /* Public link banner */
142
+ .fw-public-link {
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: space-between;
146
+ padding: 1rem;
147
+ background: var(--color-offset, #f5f5f5);
148
+ border-radius: 0.5rem;
149
+ margin-top: 2rem;
150
+ }
151
+ .fw-public-link p {
152
+ margin: 0;
153
+ color: var(--color-text-secondary, #666);
154
+ }
140
155
  </style>
141
156
 
142
157
  {% if error %}
@@ -195,11 +210,6 @@
195
210
  <span class="fw-stat-label">{{ __("funkwhale.artists") }}</span>
196
211
  </div>
197
212
  </div>
198
- {{ button({
199
- classes: "button--secondary",
200
- href: mountPath + "/stats",
201
- text: __("funkwhale.viewStats")
202
- }) }}
203
213
  </section>
204
214
  {% endif %}
205
215
 
@@ -229,11 +239,6 @@
229
239
  </li>
230
240
  {% endfor %}
231
241
  </ul>
232
- {{ button({
233
- classes: "button--secondary",
234
- href: mountPath + "/listenings",
235
- text: __("funkwhale.viewAll")
236
- }) }}
237
242
  {% else %}
238
243
  {{ prose({ text: __("funkwhale.noRecentPlays") }) }}
239
244
  {% endif %}
@@ -262,14 +267,19 @@
262
267
  </li>
263
268
  {% endfor %}
264
269
  </ul>
265
- {{ button({
266
- classes: "button--secondary",
267
- href: mountPath + "/favorites",
268
- text: __("funkwhale.viewAll")
269
- }) }}
270
270
  {% else %}
271
271
  {{ prose({ text: __("funkwhale.noFavorites") }) }}
272
272
  {% endif %}
273
273
  </section>
274
+
275
+ {# Link to public page #}
276
+ <div class="fw-public-link">
277
+ <p>{{ __("funkwhale.widget.description") }}</p>
278
+ {{ button({
279
+ href: publicUrl,
280
+ text: __("funkwhale.widget.view"),
281
+ target: "_blank"
282
+ }) }}
283
+ </div>
274
284
  {% endif %}
275
285
  {% endblock %}
package/assets/styles.css DELETED
@@ -1,453 +0,0 @@
1
- /* Funkwhale Endpoint Styles */
2
-
3
- /* Sections */
4
- .funkwhale-section {
5
- margin-block-end: 2rem;
6
- }
7
-
8
- .funkwhale-section h2 {
9
- margin-block-end: 1rem;
10
- }
11
-
12
- /* Now Playing Animation */
13
- .funkwhale-status {
14
- display: inline-flex;
15
- align-items: center;
16
- gap: 0.5rem;
17
- }
18
-
19
- .funkwhale-status--playing {
20
- color: var(--color-accent, #22c55e);
21
- }
22
-
23
- .funkwhale-bars {
24
- display: flex;
25
- gap: 2px;
26
- align-items: flex-end;
27
- height: 16px;
28
- }
29
-
30
- .funkwhale-bars span {
31
- width: 3px;
32
- background: currentColor;
33
- animation: funkwhale-bar 0.5s infinite ease-in-out alternate;
34
- }
35
-
36
- .funkwhale-bars span:nth-child(2) {
37
- animation-delay: 0.2s;
38
- }
39
-
40
- .funkwhale-bars span:nth-child(3) {
41
- animation-delay: 0.4s;
42
- }
43
-
44
- @keyframes funkwhale-bar {
45
- from { height: 4px; }
46
- to { height: 16px; }
47
- }
48
-
49
- /* Featured Track */
50
- .funkwhale-track--featured {
51
- display: flex;
52
- gap: 1rem;
53
- padding: 1rem;
54
- background: var(--color-offset, #f5f5f5);
55
- border-radius: 0.5rem;
56
- }
57
-
58
- .funkwhale-track__cover {
59
- width: 80px;
60
- height: 80px;
61
- object-fit: cover;
62
- border-radius: 0.25rem;
63
- }
64
-
65
- .funkwhale-track__cover--placeholder {
66
- background: var(--color-border, #ddd);
67
- }
68
-
69
- .funkwhale-track__info {
70
- flex: 1;
71
- display: flex;
72
- flex-direction: column;
73
- gap: 0.25rem;
74
- }
75
-
76
- .funkwhale-track__title {
77
- font-weight: 600;
78
- text-decoration: none;
79
- color: var(--color-text, inherit);
80
- }
81
-
82
- .funkwhale-track__title:hover {
83
- text-decoration: underline;
84
- }
85
-
86
- .funkwhale-track__album {
87
- margin: 0;
88
- color: var(--color-text-muted, #666);
89
- font-size: 0.875rem;
90
- }
91
-
92
- /* Stats Grid */
93
- .funkwhale-stats-grid {
94
- display: grid;
95
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
96
- gap: 1rem;
97
- margin-block-end: 1rem;
98
- }
99
-
100
- .funkwhale-stat {
101
- display: flex;
102
- flex-direction: column;
103
- align-items: center;
104
- padding: 1rem;
105
- background: var(--color-offset, #f5f5f5);
106
- border-radius: 0.5rem;
107
- text-align: center;
108
- }
109
-
110
- .funkwhale-stat__value {
111
- font-size: 1.5rem;
112
- font-weight: 700;
113
- color: var(--color-accent, #3b82f6);
114
- }
115
-
116
- .funkwhale-stat__label {
117
- font-size: 0.75rem;
118
- color: var(--color-text-muted, #666);
119
- text-transform: uppercase;
120
- letter-spacing: 0.05em;
121
- }
122
-
123
- /* List */
124
- .funkwhale-list {
125
- list-style: none;
126
- padding: 0;
127
- margin: 0;
128
- }
129
-
130
- .funkwhale-list__item {
131
- display: flex;
132
- align-items: center;
133
- gap: 0.75rem;
134
- padding: 0.75rem 0;
135
- border-bottom: 1px solid var(--color-border, #eee);
136
- }
137
-
138
- .funkwhale-list__item:last-child {
139
- border-bottom: none;
140
- }
141
-
142
- .funkwhale-list__cover {
143
- width: 48px;
144
- height: 48px;
145
- object-fit: cover;
146
- border-radius: 0.25rem;
147
- flex-shrink: 0;
148
- }
149
-
150
- .funkwhale-list__cover--placeholder {
151
- background: var(--color-border, #ddd);
152
- }
153
-
154
- .funkwhale-list__info {
155
- flex: 1;
156
- min-width: 0;
157
- }
158
-
159
- .funkwhale-list__title {
160
- display: block;
161
- font-weight: 500;
162
- text-decoration: none;
163
- color: var(--color-text, inherit);
164
- white-space: nowrap;
165
- overflow: hidden;
166
- text-overflow: ellipsis;
167
- }
168
-
169
- .funkwhale-list__title:hover {
170
- text-decoration: underline;
171
- }
172
-
173
- .funkwhale-list__album {
174
- margin: 0;
175
- font-size: 0.875rem;
176
- color: var(--color-text-muted, #666);
177
- }
178
-
179
- .funkwhale-meta {
180
- display: flex;
181
- gap: 0.5rem;
182
- color: var(--color-text-muted, #666);
183
- font-size: 0.75rem;
184
- }
185
-
186
- /* Pagination */
187
- .funkwhale-pagination {
188
- display: flex;
189
- align-items: center;
190
- justify-content: center;
191
- gap: 1rem;
192
- margin-block: 1.5rem;
193
- }
194
-
195
- .funkwhale-pagination__info {
196
- color: var(--color-text-muted, #666);
197
- }
198
-
199
- /* Tabs */
200
- .funkwhale-tabs {
201
- display: flex;
202
- gap: 0.25rem;
203
- margin-block-end: 1.5rem;
204
- border-bottom: 1px solid var(--color-border, #ddd);
205
- }
206
-
207
- .funkwhale-tab {
208
- padding: 0.75rem 1rem;
209
- border: none;
210
- background: none;
211
- color: var(--color-text-muted, #666);
212
- cursor: pointer;
213
- border-bottom: 2px solid transparent;
214
- margin-bottom: -1px;
215
- font-size: 0.875rem;
216
- font-weight: 500;
217
- }
218
-
219
- .funkwhale-tab:hover {
220
- color: var(--color-text, inherit);
221
- }
222
-
223
- .funkwhale-tab--active {
224
- color: var(--color-accent, #3b82f6);
225
- border-bottom-color: var(--color-accent, #3b82f6);
226
- }
227
-
228
- .funkwhale-tab-content {
229
- display: none;
230
- }
231
-
232
- .funkwhale-tab-content--active {
233
- display: block;
234
- }
235
-
236
- /* Top List */
237
- .funkwhale-top-list {
238
- list-style: none;
239
- padding: 0;
240
- margin: 0 0 1.5rem 0;
241
- counter-reset: rank;
242
- }
243
-
244
- .funkwhale-top-list__item {
245
- display: flex;
246
- align-items: center;
247
- gap: 0.75rem;
248
- padding: 0.5rem 0;
249
- border-bottom: 1px solid var(--color-border, #eee);
250
- }
251
-
252
- .funkwhale-top-list__rank {
253
- width: 1.5rem;
254
- height: 1.5rem;
255
- display: flex;
256
- align-items: center;
257
- justify-content: center;
258
- font-size: 0.75rem;
259
- font-weight: 600;
260
- background: var(--color-offset, #f5f5f5);
261
- border-radius: 50%;
262
- color: var(--color-text-muted, #666);
263
- }
264
-
265
- .funkwhale-top-list__name {
266
- flex: 1;
267
- font-weight: 500;
268
- }
269
-
270
- .funkwhale-top-list__count {
271
- font-size: 0.875rem;
272
- color: var(--color-text-muted, #666);
273
- }
274
-
275
- /* Album Grid */
276
- .funkwhale-album-grid {
277
- display: grid;
278
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
279
- gap: 1rem;
280
- list-style: none;
281
- padding: 0;
282
- margin: 0 0 1.5rem 0;
283
- }
284
-
285
- .funkwhale-album {
286
- display: flex;
287
- flex-direction: column;
288
- gap: 0.5rem;
289
- }
290
-
291
- .funkwhale-album__cover {
292
- width: 100%;
293
- aspect-ratio: 1;
294
- object-fit: cover;
295
- border-radius: 0.25rem;
296
- }
297
-
298
- .funkwhale-album__cover--placeholder {
299
- background: var(--color-border, #ddd);
300
- }
301
-
302
- .funkwhale-album__info {
303
- display: flex;
304
- flex-direction: column;
305
- gap: 0.125rem;
306
- }
307
-
308
- .funkwhale-album__title {
309
- font-weight: 500;
310
- font-size: 0.875rem;
311
- white-space: nowrap;
312
- overflow: hidden;
313
- text-overflow: ellipsis;
314
- }
315
-
316
- .funkwhale-album__artist {
317
- font-size: 0.75rem;
318
- color: var(--color-text-muted, #666);
319
- white-space: nowrap;
320
- overflow: hidden;
321
- text-overflow: ellipsis;
322
- }
323
-
324
- /* Trend Chart */
325
- .funkwhale-chart {
326
- margin: 1rem 0;
327
- }
328
-
329
- .funkwhale-chart__bars {
330
- display: flex;
331
- align-items: flex-end;
332
- gap: 2px;
333
- height: 120px;
334
- padding: 0.5rem 0;
335
- }
336
-
337
- .funkwhale-chart__bar-wrapper {
338
- flex: 1;
339
- height: 100%;
340
- display: flex;
341
- align-items: flex-end;
342
- }
343
-
344
- .funkwhale-chart__bar {
345
- width: 100%;
346
- background: var(--color-accent, #3b82f6);
347
- border-radius: 2px 2px 0 0;
348
- min-height: 2px;
349
- transition: height 0.3s ease;
350
- }
351
-
352
- .funkwhale-chart__bar-wrapper:hover .funkwhale-chart__bar {
353
- background: var(--color-accent-hover, #2563eb);
354
- }
355
-
356
- .funkwhale-chart__labels {
357
- display: flex;
358
- justify-content: space-between;
359
- font-size: 0.75rem;
360
- color: var(--color-text-muted, #666);
361
- margin-top: 0.5rem;
362
- }
363
-
364
- /* Widget Styles (for embeddable widgets) */
365
- .funkwhale-widget {
366
- display: flex;
367
- flex-wrap: wrap;
368
- align-items: center;
369
- gap: 0.5rem;
370
- padding: 0.75rem;
371
- background: var(--color-offset, #f5f5f5);
372
- border-radius: 0.5rem;
373
- }
374
-
375
- .funkwhale-widget--playing {
376
- background: linear-gradient(135deg, #22c55e10, #22c55e05);
377
- border: 1px solid #22c55e30;
378
- }
379
-
380
- .funkwhale-widget__status {
381
- font-size: 0.625rem;
382
- text-transform: uppercase;
383
- letter-spacing: 0.1em;
384
- color: var(--color-text-muted, #666);
385
- width: 100%;
386
- }
387
-
388
- .funkwhale-widget--playing .funkwhale-widget__status {
389
- color: #22c55e;
390
- }
391
-
392
- .funkwhale-widget__cover {
393
- width: 48px;
394
- height: 48px;
395
- object-fit: cover;
396
- border-radius: 0.25rem;
397
- }
398
-
399
- .funkwhale-widget__info {
400
- flex: 1;
401
- min-width: 0;
402
- }
403
-
404
- .funkwhale-widget__title {
405
- display: block;
406
- font-weight: 500;
407
- font-size: 0.875rem;
408
- text-decoration: none;
409
- color: var(--color-text, inherit);
410
- white-space: nowrap;
411
- overflow: hidden;
412
- text-overflow: ellipsis;
413
- }
414
-
415
- .funkwhale-widget__time {
416
- font-size: 0.75rem;
417
- color: var(--color-text-muted, #666);
418
- }
419
-
420
- .funkwhale-widget__empty,
421
- .funkwhale-widget__error {
422
- font-size: 0.875rem;
423
- color: var(--color-text-muted, #666);
424
- margin: 0;
425
- }
426
-
427
- /* Stats Widget */
428
- .funkwhale-stats-widget__grid {
429
- display: grid;
430
- grid-template-columns: repeat(3, 1fr);
431
- gap: 0.5rem;
432
- }
433
-
434
- .funkwhale-stats-widget__stat {
435
- display: flex;
436
- flex-direction: column;
437
- align-items: center;
438
- padding: 0.5rem;
439
- background: var(--color-offset, #f5f5f5);
440
- border-radius: 0.25rem;
441
- }
442
-
443
- .funkwhale-stats-widget__value {
444
- font-size: 1.25rem;
445
- font-weight: 700;
446
- color: var(--color-accent, #3b82f6);
447
- }
448
-
449
- .funkwhale-stats-widget__label {
450
- font-size: 0.625rem;
451
- color: var(--color-text-muted, #666);
452
- text-transform: uppercase;
453
- }