@rmdes/indiekit-endpoint-funkwhale 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 +116 -0
- package/assets/styles.css +453 -0
- package/includes/@indiekit-endpoint-funkwhale-now-playing.njk +83 -0
- package/includes/@indiekit-endpoint-funkwhale-stats.njk +75 -0
- package/includes/@indiekit-endpoint-funkwhale-widget.njk +12 -0
- package/index.js +108 -0
- package/lib/controllers/dashboard.js +122 -0
- package/lib/controllers/favorites.js +110 -0
- package/lib/controllers/listenings.js +109 -0
- package/lib/controllers/now-playing.js +58 -0
- package/lib/controllers/stats.js +138 -0
- package/lib/funkwhale-client.js +187 -0
- package/lib/stats.js +228 -0
- package/lib/sync.js +160 -0
- package/lib/utils.js +242 -0
- package/locales/en.json +29 -0
- package/package.json +54 -0
- package/views/favorites.njk +60 -0
- package/views/funkwhale.njk +145 -0
- package/views/listenings.njk +65 -0
- package/views/partials/stats-summary.njk +18 -0
- package/views/partials/top-albums.njk +20 -0
- package/views/partials/top-artists.njk +13 -0
- package/views/stats.njk +130 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @rmdes/indiekit-endpoint-funkwhale
|
|
2
|
+
|
|
3
|
+
Funkwhale listening activity endpoint for [Indiekit](https://getindiekit.com/).
|
|
4
|
+
|
|
5
|
+
Display your Funkwhale listening history, favorite tracks, and listening statistics on your IndieWeb site.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Now Playing Widget** - Shows currently playing or recently played tracks
|
|
10
|
+
- **Listening History** - Browse your listening history with album art
|
|
11
|
+
- **Favorites** - Display your favorite tracks
|
|
12
|
+
- **Statistics** - View listening stats with tabbed interface:
|
|
13
|
+
- All Time / This Month / This Week views
|
|
14
|
+
- Top Artists and Albums
|
|
15
|
+
- Listening trend charts
|
|
16
|
+
- **Public JSON API** - For integration with static site generators like Eleventy
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @rmdes/indiekit-endpoint-funkwhale
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
Add to your `indiekit.config.js`:
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
export default {
|
|
30
|
+
plugins: [
|
|
31
|
+
"@rmdes/indiekit-endpoint-funkwhale",
|
|
32
|
+
],
|
|
33
|
+
|
|
34
|
+
"@rmdes/indiekit-endpoint-funkwhale": {
|
|
35
|
+
mountPath: "/funkwhale",
|
|
36
|
+
instanceUrl: process.env.FUNKWHALE_INSTANCE,
|
|
37
|
+
username: process.env.FUNKWHALE_USERNAME,
|
|
38
|
+
token: process.env.FUNKWHALE_TOKEN,
|
|
39
|
+
cacheTtl: 900_000, // 15 minutes
|
|
40
|
+
syncInterval: 300_000, // 5 minutes
|
|
41
|
+
limits: {
|
|
42
|
+
listenings: 20,
|
|
43
|
+
favorites: 20,
|
|
44
|
+
topArtists: 10,
|
|
45
|
+
topAlbums: 10
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Environment Variables
|
|
52
|
+
|
|
53
|
+
| Variable | Required | Description |
|
|
54
|
+
|----------|----------|-------------|
|
|
55
|
+
| `FUNKWHALE_INSTANCE` | Yes | Your Funkwhale instance URL (e.g., `https://funkwhale.example.com`) |
|
|
56
|
+
| `FUNKWHALE_TOKEN` | Yes | API access token (Bearer token) |
|
|
57
|
+
| `FUNKWHALE_USERNAME` | Yes | Your username on the Funkwhale instance |
|
|
58
|
+
|
|
59
|
+
### Getting an API Token
|
|
60
|
+
|
|
61
|
+
1. Log in to your Funkwhale instance
|
|
62
|
+
2. Go to Settings > Applications
|
|
63
|
+
3. Create a new application with read permissions
|
|
64
|
+
4. Copy the access token
|
|
65
|
+
|
|
66
|
+
## Routes
|
|
67
|
+
|
|
68
|
+
### Protected Routes (require authentication)
|
|
69
|
+
|
|
70
|
+
| Route | Description |
|
|
71
|
+
|-------|-------------|
|
|
72
|
+
| `GET /funkwhale/` | Dashboard with overview |
|
|
73
|
+
| `GET /funkwhale/listenings` | Full listening history |
|
|
74
|
+
| `GET /funkwhale/favorites` | Favorite tracks |
|
|
75
|
+
| `GET /funkwhale/stats` | Statistics with tabs |
|
|
76
|
+
|
|
77
|
+
### Public API Routes (JSON)
|
|
78
|
+
|
|
79
|
+
| Route | Description |
|
|
80
|
+
|-------|-------------|
|
|
81
|
+
| `GET /funkwhale/api/now-playing` | Current/recent track |
|
|
82
|
+
| `GET /funkwhale/api/listenings` | Recent listenings |
|
|
83
|
+
| `GET /funkwhale/api/favorites` | Favorites list |
|
|
84
|
+
| `GET /funkwhale/api/stats` | All statistics |
|
|
85
|
+
| `GET /funkwhale/api/stats/trends` | Trend data for charts |
|
|
86
|
+
|
|
87
|
+
## Options
|
|
88
|
+
|
|
89
|
+
| Option | Default | Description |
|
|
90
|
+
|--------|---------|-------------|
|
|
91
|
+
| `mountPath` | `/funkwhale` | URL path for the endpoint |
|
|
92
|
+
| `instanceUrl` | - | Funkwhale instance URL |
|
|
93
|
+
| `token` | - | API access token |
|
|
94
|
+
| `username` | - | User to track |
|
|
95
|
+
| `cacheTtl` | `900000` | Cache TTL in ms (15 min) |
|
|
96
|
+
| `syncInterval` | `300000` | Background sync interval in ms (5 min) |
|
|
97
|
+
| `limits.listenings` | `20` | Listenings per page |
|
|
98
|
+
| `limits.favorites` | `20` | Favorites per page |
|
|
99
|
+
| `limits.topArtists` | `10` | Top artists to show |
|
|
100
|
+
| `limits.topAlbums` | `10` | Top albums to show |
|
|
101
|
+
|
|
102
|
+
## Now Playing Logic
|
|
103
|
+
|
|
104
|
+
- **Now Playing**: Track listened to within the last 60 minutes
|
|
105
|
+
- **Recently Played**: Track listened to within the last 24 hours
|
|
106
|
+
- **Last Played**: Older tracks show timestamp only
|
|
107
|
+
|
|
108
|
+
## Requirements
|
|
109
|
+
|
|
110
|
+
- Indiekit >= 1.0.0-beta.25
|
|
111
|
+
- MongoDB (for statistics aggregation)
|
|
112
|
+
- Funkwhale instance with API v2
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,453 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{#
|
|
2
|
+
Now Playing Widget
|
|
3
|
+
Fetches data from /funkwhale/api/now-playing
|
|
4
|
+
Include this in your Eleventy templates
|
|
5
|
+
#}
|
|
6
|
+
<div class="funkwhale-now-playing-widget" id="funkwhale-now-playing">
|
|
7
|
+
<div class="funkwhale-now-playing-widget__loading">
|
|
8
|
+
Loading...
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
(function() {
|
|
14
|
+
const container = document.getElementById('funkwhale-now-playing');
|
|
15
|
+
const endpoint = '{{ application.funkwhaleEndpoint or "/funkwhale" }}/api/now-playing';
|
|
16
|
+
|
|
17
|
+
fetch(endpoint)
|
|
18
|
+
.then(res => res.json())
|
|
19
|
+
.then(data => {
|
|
20
|
+
container.textContent = '';
|
|
21
|
+
|
|
22
|
+
if (!data.track) {
|
|
23
|
+
const empty = document.createElement('p');
|
|
24
|
+
empty.className = 'funkwhale-widget__empty';
|
|
25
|
+
empty.textContent = 'No recent plays';
|
|
26
|
+
container.appendChild(empty);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const widget = document.createElement('div');
|
|
31
|
+
widget.className = 'funkwhale-widget' + (data.status === 'now-playing' ? ' funkwhale-widget--playing' : '');
|
|
32
|
+
|
|
33
|
+
if (data.status === 'now-playing') {
|
|
34
|
+
const bars = document.createElement('div');
|
|
35
|
+
bars.className = 'funkwhale-bars';
|
|
36
|
+
for (let i = 0; i < 3; i++) {
|
|
37
|
+
bars.appendChild(document.createElement('span'));
|
|
38
|
+
}
|
|
39
|
+
widget.appendChild(bars);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const status = document.createElement('span');
|
|
43
|
+
status.className = 'funkwhale-widget__status';
|
|
44
|
+
status.textContent = data.status === 'now-playing' ? 'Now Playing' :
|
|
45
|
+
data.status === 'recently-played' ? 'Recently Played' : 'Last Played';
|
|
46
|
+
widget.appendChild(status);
|
|
47
|
+
|
|
48
|
+
if (data.coverUrl) {
|
|
49
|
+
const img = document.createElement('img');
|
|
50
|
+
img.src = data.coverUrl;
|
|
51
|
+
img.alt = '';
|
|
52
|
+
img.className = 'funkwhale-widget__cover';
|
|
53
|
+
widget.appendChild(img);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const info = document.createElement('div');
|
|
57
|
+
info.className = 'funkwhale-widget__info';
|
|
58
|
+
|
|
59
|
+
const link = document.createElement('a');
|
|
60
|
+
link.href = data.trackUrl;
|
|
61
|
+
link.className = 'funkwhale-widget__title';
|
|
62
|
+
link.target = '_blank';
|
|
63
|
+
link.rel = 'noopener';
|
|
64
|
+
link.textContent = data.artist + ' - ' + data.track;
|
|
65
|
+
info.appendChild(link);
|
|
66
|
+
|
|
67
|
+
const time = document.createElement('span');
|
|
68
|
+
time.className = 'funkwhale-widget__time';
|
|
69
|
+
time.textContent = data.relativeTime;
|
|
70
|
+
info.appendChild(time);
|
|
71
|
+
|
|
72
|
+
widget.appendChild(info);
|
|
73
|
+
container.appendChild(widget);
|
|
74
|
+
})
|
|
75
|
+
.catch(err => {
|
|
76
|
+
container.textContent = '';
|
|
77
|
+
const error = document.createElement('p');
|
|
78
|
+
error.className = 'funkwhale-widget__error';
|
|
79
|
+
error.textContent = 'Could not load';
|
|
80
|
+
container.appendChild(error);
|
|
81
|
+
});
|
|
82
|
+
})();
|
|
83
|
+
</script>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{#
|
|
2
|
+
Stats Widget
|
|
3
|
+
Fetches data from /funkwhale/api/stats
|
|
4
|
+
Include this in your Eleventy templates for a sidebar widget
|
|
5
|
+
#}
|
|
6
|
+
<div class="funkwhale-stats-widget" id="funkwhale-stats">
|
|
7
|
+
<div class="funkwhale-stats-widget__loading">
|
|
8
|
+
Loading stats...
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
(function() {
|
|
14
|
+
const container = document.getElementById('funkwhale-stats');
|
|
15
|
+
const endpoint = '{{ application.funkwhaleEndpoint or "/funkwhale" }}/api/stats';
|
|
16
|
+
|
|
17
|
+
fetch(endpoint)
|
|
18
|
+
.then(res => res.json())
|
|
19
|
+
.then(data => {
|
|
20
|
+
container.textContent = '';
|
|
21
|
+
const all = data.summary?.all || {};
|
|
22
|
+
|
|
23
|
+
const grid = document.createElement('div');
|
|
24
|
+
grid.className = 'funkwhale-stats-widget__grid';
|
|
25
|
+
|
|
26
|
+
// Plays stat
|
|
27
|
+
const playsStat = document.createElement('div');
|
|
28
|
+
playsStat.className = 'funkwhale-stats-widget__stat';
|
|
29
|
+
const playsValue = document.createElement('span');
|
|
30
|
+
playsValue.className = 'funkwhale-stats-widget__value';
|
|
31
|
+
playsValue.textContent = all.totalPlays || 0;
|
|
32
|
+
const playsLabel = document.createElement('span');
|
|
33
|
+
playsLabel.className = 'funkwhale-stats-widget__label';
|
|
34
|
+
playsLabel.textContent = 'plays';
|
|
35
|
+
playsStat.appendChild(playsValue);
|
|
36
|
+
playsStat.appendChild(playsLabel);
|
|
37
|
+
grid.appendChild(playsStat);
|
|
38
|
+
|
|
39
|
+
// Artists stat
|
|
40
|
+
const artistsStat = document.createElement('div');
|
|
41
|
+
artistsStat.className = 'funkwhale-stats-widget__stat';
|
|
42
|
+
const artistsValue = document.createElement('span');
|
|
43
|
+
artistsValue.className = 'funkwhale-stats-widget__value';
|
|
44
|
+
artistsValue.textContent = all.uniqueArtists || 0;
|
|
45
|
+
const artistsLabel = document.createElement('span');
|
|
46
|
+
artistsLabel.className = 'funkwhale-stats-widget__label';
|
|
47
|
+
artistsLabel.textContent = 'artists';
|
|
48
|
+
artistsStat.appendChild(artistsValue);
|
|
49
|
+
artistsStat.appendChild(artistsLabel);
|
|
50
|
+
grid.appendChild(artistsStat);
|
|
51
|
+
|
|
52
|
+
// Duration stat
|
|
53
|
+
const durationStat = document.createElement('div');
|
|
54
|
+
durationStat.className = 'funkwhale-stats-widget__stat';
|
|
55
|
+
const durationValue = document.createElement('span');
|
|
56
|
+
durationValue.className = 'funkwhale-stats-widget__value';
|
|
57
|
+
durationValue.textContent = all.totalDurationFormatted || '0h';
|
|
58
|
+
const durationLabel = document.createElement('span');
|
|
59
|
+
durationLabel.className = 'funkwhale-stats-widget__label';
|
|
60
|
+
durationLabel.textContent = 'listened';
|
|
61
|
+
durationStat.appendChild(durationValue);
|
|
62
|
+
durationStat.appendChild(durationLabel);
|
|
63
|
+
grid.appendChild(durationStat);
|
|
64
|
+
|
|
65
|
+
container.appendChild(grid);
|
|
66
|
+
})
|
|
67
|
+
.catch(err => {
|
|
68
|
+
container.textContent = '';
|
|
69
|
+
const error = document.createElement('p');
|
|
70
|
+
error.className = 'funkwhale-widget__error';
|
|
71
|
+
error.textContent = 'Could not load stats';
|
|
72
|
+
container.appendChild(error);
|
|
73
|
+
});
|
|
74
|
+
})();
|
|
75
|
+
</script>
|