@rmdes/indiekit-endpoint-funkwhale 1.0.0 → 1.0.2
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 +12 -0
- package/lib/controllers/stats.js +29 -9
- package/lib/sync.js +39 -1
- package/locales/en.json +35 -27
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
|
|
3
5
|
import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
4
6
|
import { listeningsController } from "./lib/controllers/listenings.js";
|
|
@@ -7,6 +9,8 @@ import { statsController } from "./lib/controllers/stats.js";
|
|
|
7
9
|
import { nowPlayingController } from "./lib/controllers/now-playing.js";
|
|
8
10
|
import { startSync } from "./lib/sync.js";
|
|
9
11
|
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
10
14
|
const protectedRouter = express.Router();
|
|
11
15
|
const publicRouter = express.Router();
|
|
12
16
|
|
|
@@ -37,6 +41,14 @@ export default class FunkwhaleEndpoint {
|
|
|
37
41
|
return ["FUNKWHALE_TOKEN", "FUNKWHALE_INSTANCE", "FUNKWHALE_USERNAME"];
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
get localesDirectory() {
|
|
45
|
+
return path.join(__dirname, "locales");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get styles() {
|
|
49
|
+
return path.join(__dirname, "assets", "styles.css");
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
get navigationItems() {
|
|
41
53
|
return {
|
|
42
54
|
href: this.options.mountPath,
|
package/lib/controllers/stats.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAllStats, getListeningTrends } from "../stats.js";
|
|
2
|
+
import { getCachedStats } from "../sync.js";
|
|
2
3
|
import { formatTotalTime } from "../utils.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -84,13 +85,23 @@ export const statsController = {
|
|
|
84
85
|
return response.status(500).json({ error: "Not configured" });
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// Try database first, fall back to cache for public routes
|
|
87
89
|
const db = request.app.locals.database;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
let stats;
|
|
91
|
+
|
|
92
|
+
if (db) {
|
|
93
|
+
stats = await getAllStats(db, funkwhaleConfig.limits);
|
|
94
|
+
} else {
|
|
95
|
+
// Public routes don't have DB access, use cached stats
|
|
96
|
+
stats = getCachedStats();
|
|
97
|
+
if (!stats) {
|
|
98
|
+
return response.status(503).json({
|
|
99
|
+
error: "Stats not available yet",
|
|
100
|
+
message: "Stats are computed during background sync. Please try again shortly.",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
const stats = await getAllStats(db, funkwhaleConfig.limits);
|
|
93
|
-
|
|
94
105
|
// Add formatted durations
|
|
95
106
|
stats.summary.all.totalDurationFormatted = formatTotalTime(
|
|
96
107
|
stats.summary.all.totalDuration
|
|
@@ -122,14 +133,23 @@ export const statsController = {
|
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
const db = request.app.locals.database;
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
const days = Math.min(parseInt(request.query.days) || 30, 90);
|
|
137
|
+
|
|
138
|
+
if (db) {
|
|
139
|
+
const trends = await getListeningTrends(db, days);
|
|
140
|
+
return response.json({ trends, days });
|
|
127
141
|
}
|
|
128
142
|
|
|
129
|
-
|
|
130
|
-
const
|
|
143
|
+
// Fall back to cached stats for public routes
|
|
144
|
+
const cachedStats = getCachedStats();
|
|
145
|
+
if (cachedStats?.trends) {
|
|
146
|
+
return response.json({ trends: cachedStats.trends, days: 30 });
|
|
147
|
+
}
|
|
131
148
|
|
|
132
|
-
response.json({
|
|
149
|
+
return response.status(503).json({
|
|
150
|
+
error: "Trends not available yet",
|
|
151
|
+
message: "Trends are computed during background sync. Please try again shortly.",
|
|
152
|
+
});
|
|
133
153
|
} catch (error) {
|
|
134
154
|
console.error("[Funkwhale] Trends API error:", error);
|
|
135
155
|
response.status(500).json({ error: error.message });
|
package/lib/sync.js
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import { FunkwhaleClient } from "./funkwhale-client.js";
|
|
2
2
|
import { getCoverUrl, getArtistName } from "./utils.js";
|
|
3
|
+
import { getAllStats, getListeningTrends } from "./stats.js";
|
|
3
4
|
|
|
4
5
|
let syncInterval = null;
|
|
5
6
|
|
|
7
|
+
// In-memory cache for stats (accessible to public routes)
|
|
8
|
+
let cachedStats = null;
|
|
9
|
+
let cachedStatsTime = null;
|
|
10
|
+
const STATS_CACHE_TTL = 300_000; // 5 minutes
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get cached stats (for public API routes that can't access DB)
|
|
14
|
+
* @returns {object|null} - Cached stats or null
|
|
15
|
+
*/
|
|
16
|
+
export function getCachedStats() {
|
|
17
|
+
if (!cachedStats) return null;
|
|
18
|
+
if (cachedStatsTime && Date.now() - cachedStatsTime > STATS_CACHE_TTL) {
|
|
19
|
+
return cachedStats; // Return stale cache, sync will refresh
|
|
20
|
+
}
|
|
21
|
+
return cachedStats;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Update stats cache
|
|
26
|
+
* @param {object} stats - Stats to cache
|
|
27
|
+
*/
|
|
28
|
+
export function setCachedStats(stats) {
|
|
29
|
+
cachedStats = stats;
|
|
30
|
+
cachedStatsTime = Date.now();
|
|
31
|
+
}
|
|
32
|
+
|
|
6
33
|
/**
|
|
7
34
|
* Start background sync process
|
|
8
35
|
* @param {object} Indiekit - Indiekit instance
|
|
@@ -61,7 +88,18 @@ export async function runSync(Indiekit, options) {
|
|
|
61
88
|
cacheTtl: 60_000, // Short cache for sync
|
|
62
89
|
});
|
|
63
90
|
|
|
64
|
-
|
|
91
|
+
const result = await syncListenings(db, client);
|
|
92
|
+
|
|
93
|
+
// Update stats cache after sync
|
|
94
|
+
try {
|
|
95
|
+
const stats = await getAllStats(db, options.limits || {});
|
|
96
|
+
setCachedStats(stats);
|
|
97
|
+
console.log("[Funkwhale] Stats cache updated");
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error("[Funkwhale] Failed to cache stats:", err.message);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
65
103
|
}
|
|
66
104
|
|
|
67
105
|
/**
|
package/locales/en.json
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
{
|
|
2
|
-
"funkwhale
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
2
|
+
"funkwhale": {
|
|
3
|
+
"title": "Funkwhale",
|
|
4
|
+
"listenings": "Listening History",
|
|
5
|
+
"favorites": "Favorites",
|
|
6
|
+
"stats": "Statistics",
|
|
7
|
+
"nowPlaying": "Now Playing",
|
|
8
|
+
"recentlyPlayed": "Recently Played",
|
|
9
|
+
"lastPlayed": "Last Played",
|
|
10
|
+
"allTime": "All Time",
|
|
11
|
+
"thisWeek": "This Week",
|
|
12
|
+
"thisMonth": "This Month",
|
|
13
|
+
"trends": "Trends",
|
|
14
|
+
"listeningTrend": "Listening Trend (30 days)",
|
|
15
|
+
"topArtists": "Top Artists",
|
|
16
|
+
"topAlbums": "Top Albums",
|
|
17
|
+
"plays": "plays",
|
|
18
|
+
"tracks": "tracks",
|
|
19
|
+
"artists": "artists",
|
|
20
|
+
"albums": "albums",
|
|
21
|
+
"listeningTime": "Listening Time",
|
|
22
|
+
"noRecentPlays": "No music played recently",
|
|
23
|
+
"noFavorites": "No favorite tracks yet",
|
|
24
|
+
"viewAll": "View All",
|
|
25
|
+
"viewStats": "View Statistics",
|
|
26
|
+
"sync": "Sync Now",
|
|
27
|
+
"lastSync": "Last sync",
|
|
28
|
+
"error": {
|
|
29
|
+
"connection": "Could not connect to Funkwhale",
|
|
30
|
+
"noConfig": "Funkwhale endpoint not configured correctly"
|
|
31
|
+
},
|
|
32
|
+
"widget": {
|
|
33
|
+
"description": "View your Funkwhale listening activity",
|
|
34
|
+
"view": "View Activity"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
29
37
|
}
|
package/package.json
CHANGED