@akilles/soundcloud-watcher 2.2.3 → 2.3.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 +2 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -4
- package/soundcloud_watcher.ts +45 -9
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Monitor your SoundCloud account and track artist releases. Get notified when som
|
|
|
9
9
|
- **New releases** - Get notifications when tracked artists release new music
|
|
10
10
|
- **Smart API usage** - Only fetches what changed, automatically skips dormant artists
|
|
11
11
|
- **Rate limit handling** - Exponential backoff for API reliability
|
|
12
|
+
- **OAuth 2.1** - Uses SoundCloud's latest authentication endpoint
|
|
12
13
|
|
|
13
14
|
## Prerequisites
|
|
14
15
|
|
|
@@ -85,7 +86,7 @@ The plugin responds to commands but doesn't auto-poll. Set up a cron job for aut
|
|
|
85
86
|
openclaw cron add --name "soundcloud-check" \
|
|
86
87
|
--every 6h \
|
|
87
88
|
--isolated \
|
|
88
|
-
--message "Run /soundcloud-cron and forward any updates to me
|
|
89
|
+
--message "Run /soundcloud-cron and forward any updates to me."
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
Uses `/soundcloud-cron` which:
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akilles/soundcloud-watcher",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "OpenClaw plugin to monitor SoundCloud account and track artist releases",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"openclaw": {
|
|
@@ -39,13 +39,11 @@
|
|
|
39
39
|
"README.md",
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
|
-
"peerDependencies": {
|
|
43
|
-
"@types/node": "^22.0.0"
|
|
44
|
-
},
|
|
45
42
|
"engines": {
|
|
46
43
|
"node": ">=22.0.0"
|
|
47
44
|
},
|
|
48
45
|
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
49
47
|
"typescript": "^5.9.3"
|
|
50
48
|
}
|
|
51
49
|
}
|
package/soundcloud_watcher.ts
CHANGED
|
@@ -75,8 +75,9 @@ interface UserInfo {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
interface FollowerNotification {
|
|
78
|
-
type: 'new' | 'lost';
|
|
78
|
+
type: 'new' | 'lost' | 'renamed';
|
|
79
79
|
users: UserInfo[];
|
|
80
|
+
renames?: { old: UserInfo; new: UserInfo }[]; // Only for type 'renamed'
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
interface AccountNotifications {
|
|
@@ -335,14 +336,19 @@ class SoundCloudAPI {
|
|
|
335
336
|
|
|
336
337
|
try {
|
|
337
338
|
this.log("Refreshing token...");
|
|
339
|
+
const basicAuth = Buffer.from(
|
|
340
|
+
`${this.config.clientId}:${this.config.clientSecret}`
|
|
341
|
+
).toString("base64");
|
|
338
342
|
const body = new URLSearchParams({
|
|
339
343
|
grant_type: "client_credentials",
|
|
340
|
-
client_id: this.config.clientId,
|
|
341
|
-
client_secret: this.config.clientSecret,
|
|
342
344
|
});
|
|
343
|
-
const resp = await fetch(
|
|
345
|
+
const resp = await fetch("https://secure.soundcloud.com/oauth/token", {
|
|
344
346
|
method: "POST",
|
|
345
|
-
headers: {
|
|
347
|
+
headers: {
|
|
348
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
349
|
+
"Authorization": `Basic ${basicAuth}`,
|
|
350
|
+
"Accept": "application/json; charset=utf-8",
|
|
351
|
+
},
|
|
346
352
|
body: body.toString(),
|
|
347
353
|
signal: AbortSignal.timeout(API_TIMEOUT_MS),
|
|
348
354
|
});
|
|
@@ -576,6 +582,19 @@ class AccountWatcher {
|
|
|
576
582
|
const lostFollowers = Object.entries(stored)
|
|
577
583
|
.filter(([uid]) => !currentFollowers[uid])
|
|
578
584
|
.map(([, f]) => f);
|
|
585
|
+
|
|
586
|
+
// Detect name changes for existing followers
|
|
587
|
+
const renames: { old: UserInfo; new: UserInfo }[] = [];
|
|
588
|
+
for (const [uid, current] of Object.entries(currentFollowers)) {
|
|
589
|
+
const prev = stored[uid];
|
|
590
|
+
if (prev) {
|
|
591
|
+
const nameChanged = prev.username !== current.username ||
|
|
592
|
+
prev.display_name !== current.display_name;
|
|
593
|
+
if (nameChanged) {
|
|
594
|
+
renames.push({ old: prev, new: current });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
579
598
|
|
|
580
599
|
if (newFollowers.length) {
|
|
581
600
|
result.followers.push({ type: 'new', users: newFollowers });
|
|
@@ -583,6 +602,9 @@ class AccountWatcher {
|
|
|
583
602
|
if (lostFollowers.length) {
|
|
584
603
|
result.followers.push({ type: 'lost', users: lostFollowers });
|
|
585
604
|
}
|
|
605
|
+
if (renames.length) {
|
|
606
|
+
result.followers.push({ type: 'renamed', users: [], renames });
|
|
607
|
+
}
|
|
586
608
|
}
|
|
587
609
|
|
|
588
610
|
this.data.my_followers = currentFollowers;
|
|
@@ -974,20 +996,34 @@ export class SoundCloudWatcher {
|
|
|
974
996
|
const users = notif.users.slice(0, 5); // Max 5 users shown
|
|
975
997
|
const remaining = notif.users.length - users.length;
|
|
976
998
|
|
|
999
|
+
// Helper: use display_name if set, otherwise fall back to @username
|
|
1000
|
+
const getName = (u: UserInfo) => u.display_name?.trim() || `@${u.username}`;
|
|
1001
|
+
|
|
977
1002
|
if (notif.type === 'new') {
|
|
978
1003
|
lines.push(`New follower${notif.users.length > 1 ? 's' : ''}:`);
|
|
979
1004
|
for (const u of users) {
|
|
980
1005
|
if (this.includeLinks && u.permalink_url) {
|
|
981
|
-
lines.push(`- **${u
|
|
1006
|
+
lines.push(`- **${getName(u)}**: ${u.permalink_url}`);
|
|
982
1007
|
} else {
|
|
983
|
-
lines.push(`- **${u
|
|
1008
|
+
lines.push(`- **${getName(u)}**`);
|
|
984
1009
|
}
|
|
985
1010
|
}
|
|
986
|
-
} else {
|
|
1011
|
+
} else if (notif.type === 'lost') {
|
|
987
1012
|
lines.push(`Lost follower${notif.users.length > 1 ? 's' : ''}:`);
|
|
988
1013
|
for (const u of users) {
|
|
989
|
-
lines.push(`- ${u
|
|
1014
|
+
lines.push(`- ${getName(u)}`);
|
|
1015
|
+
}
|
|
1016
|
+
} else if (notif.type === 'renamed' && notif.renames?.length) {
|
|
1017
|
+
const renames = notif.renames.slice(0, 5);
|
|
1018
|
+
const renameRemaining = (notif.renames?.length ?? 0) - renames.length;
|
|
1019
|
+
lines.push(`Name change${renames.length > 1 ? 's' : ''}:`);
|
|
1020
|
+
for (const r of renames) {
|
|
1021
|
+
lines.push(`- ${getName(r.old)} → ${getName(r.new)}`);
|
|
1022
|
+
}
|
|
1023
|
+
if (renameRemaining > 0) {
|
|
1024
|
+
lines.push(` ...and ${renameRemaining} more`);
|
|
990
1025
|
}
|
|
1026
|
+
return lines; // Early return, skip the standard remaining count
|
|
991
1027
|
}
|
|
992
1028
|
|
|
993
1029
|
if (remaining > 0) {
|