@akilles/soundcloud-watcher 2.0.5 → 2.1.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 +3 -0
- package/index.ts +3 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/soundcloud_watcher.ts +92 -27
package/README.md
CHANGED
|
@@ -48,6 +48,9 @@ Add your credentials:
|
|
|
48
48
|
SOUNDCLOUD_CLIENT_ID=your_client_id
|
|
49
49
|
SOUNDCLOUD_CLIENT_SECRET=your_client_secret
|
|
50
50
|
MY_USERNAME=your_soundcloud_username
|
|
51
|
+
|
|
52
|
+
# Optional settings
|
|
53
|
+
INCLUDE_LINKS=true # Include URLs in notifications (default: true)
|
|
51
54
|
```
|
|
52
55
|
|
|
53
56
|
### 4. Restart & Verify
|
package/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ interface PluginConfig {
|
|
|
10
10
|
checkIntervalHours: number;
|
|
11
11
|
myTracksLimit: number;
|
|
12
12
|
dormantDays: number;
|
|
13
|
+
includeLinks: boolean;
|
|
13
14
|
sessionKey?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -34,6 +35,7 @@ function loadConfig(): PluginConfig | null {
|
|
|
34
35
|
checkIntervalHours: 6,
|
|
35
36
|
myTracksLimit: 10,
|
|
36
37
|
dormantDays: 90,
|
|
38
|
+
includeLinks: env.INCLUDE_LINKS !== 'false', // Default: true
|
|
37
39
|
sessionKey: 'agent:main:main',
|
|
38
40
|
};
|
|
39
41
|
}
|
|
@@ -57,6 +59,7 @@ export default function register(api: any) {
|
|
|
57
59
|
username: config.username,
|
|
58
60
|
myTracksLimit: config.myTracksLimit,
|
|
59
61
|
dormantDays: config.dormantDays,
|
|
62
|
+
includeLinks: config.includeLinks,
|
|
60
63
|
logger: (...args: any[]) => logger.debug?.(...args) || console.log(...args),
|
|
61
64
|
});
|
|
62
65
|
return watcher;
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/soundcloud_watcher.ts
CHANGED
|
@@ -56,12 +56,24 @@ export interface SoundCloudWatcherConfig {
|
|
|
56
56
|
username: string;
|
|
57
57
|
myTracksLimit?: number;
|
|
58
58
|
dormantDays?: number;
|
|
59
|
+
includeLinks?: boolean; // Include URLs in notifications (default: true)
|
|
59
60
|
logger?: (...args: any[]) => void;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
interface UserInfo {
|
|
63
64
|
username: string;
|
|
64
65
|
display_name: string;
|
|
66
|
+
permalink_url?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface FollowerNotification {
|
|
70
|
+
type: 'new' | 'lost';
|
|
71
|
+
users: UserInfo[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface AccountNotifications {
|
|
75
|
+
followers: FollowerNotification[];
|
|
76
|
+
engagement: string[]; // likes, reposts, etc.
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
interface TrackStats {
|
|
@@ -396,9 +408,11 @@ class SoundCloudAPI {
|
|
|
396
408
|
|
|
397
409
|
for (const f of data.collection ?? []) {
|
|
398
410
|
if (f && typeof f === "object" && "id" in f) {
|
|
411
|
+
const permalink = f.permalink ?? f.username ?? "unknown";
|
|
399
412
|
followers[String(f.id)] = {
|
|
400
|
-
username:
|
|
413
|
+
username: permalink,
|
|
401
414
|
display_name: f.full_name ?? f.username ?? "unknown",
|
|
415
|
+
permalink_url: f.permalink_url ?? `https://soundcloud.com/${permalink}`,
|
|
402
416
|
};
|
|
403
417
|
}
|
|
404
418
|
}
|
|
@@ -442,12 +456,12 @@ class AccountWatcher {
|
|
|
442
456
|
writeJson(ACCOUNT_DATA, this.data);
|
|
443
457
|
}
|
|
444
458
|
|
|
445
|
-
async check(): Promise<
|
|
446
|
-
const
|
|
459
|
+
async check(): Promise<AccountNotifications> {
|
|
460
|
+
const result: AccountNotifications = { followers: [], engagement: [] };
|
|
447
461
|
|
|
448
462
|
if (!this.data.my_account) {
|
|
449
463
|
const user = await this.api.resolve(this.config.myUsername);
|
|
450
|
-
if (!user) return
|
|
464
|
+
if (!user) return result;
|
|
451
465
|
this.data.my_account = {
|
|
452
466
|
user_id: user.id,
|
|
453
467
|
username: user.permalink ?? this.config.myUsername,
|
|
@@ -459,7 +473,7 @@ class AccountWatcher {
|
|
|
459
473
|
const profile = await this.api.getUser(userId);
|
|
460
474
|
if (!profile) {
|
|
461
475
|
this.log("Failed to fetch profile, skipping account check");
|
|
462
|
-
return
|
|
476
|
+
return result;
|
|
463
477
|
}
|
|
464
478
|
|
|
465
479
|
const currentCount = num(profile.followers_count);
|
|
@@ -479,23 +493,16 @@ class AccountWatcher {
|
|
|
479
493
|
if (Object.keys(stored).length) {
|
|
480
494
|
const newFollowers = Object.entries(currentFollowers)
|
|
481
495
|
.filter(([uid]) => !stored[uid])
|
|
482
|
-
.map(([, f]) => f
|
|
496
|
+
.map(([, f]) => f);
|
|
483
497
|
const lostFollowers = Object.entries(stored)
|
|
484
498
|
.filter(([uid]) => !currentFollowers[uid])
|
|
485
|
-
.map(([, f]) => f
|
|
499
|
+
.map(([, f]) => f);
|
|
486
500
|
|
|
487
501
|
if (newFollowers.length) {
|
|
488
|
-
|
|
489
|
-
if (newFollowers.length > 3) names += ` +${newFollowers.length - 3} more`;
|
|
490
|
-
notifications.push(
|
|
491
|
-
`New follower${newFollowers.length > 1 ? "s" : ""}: **${names}**`
|
|
492
|
-
);
|
|
502
|
+
result.followers.push({ type: 'new', users: newFollowers });
|
|
493
503
|
}
|
|
494
504
|
if (lostFollowers.length) {
|
|
495
|
-
|
|
496
|
-
notifications.push(
|
|
497
|
-
`Lost follower${lostFollowers.length > 1 ? "s" : ""}: ${names}`
|
|
498
|
-
);
|
|
505
|
+
result.followers.push({ type: 'lost', users: lostFollowers });
|
|
499
506
|
}
|
|
500
507
|
}
|
|
501
508
|
|
|
@@ -550,11 +557,11 @@ class AccountWatcher {
|
|
|
550
557
|
let names = newLikerNames.slice(0, 3).join(", ");
|
|
551
558
|
if (newLikerNames.length > 3)
|
|
552
559
|
names += ` +${newLikerNames.length - 3} more`;
|
|
553
|
-
|
|
560
|
+
result.engagement.push(`**${names}** liked '${title}'`);
|
|
554
561
|
}
|
|
555
562
|
if (unlikerNames.length) {
|
|
556
563
|
const names = unlikerNames.slice(0, 3).join(", ");
|
|
557
|
-
|
|
564
|
+
result.engagement.push(`${names} unliked '${title}'`);
|
|
558
565
|
}
|
|
559
566
|
} else {
|
|
560
567
|
stats.likers = prevLikers;
|
|
@@ -562,7 +569,7 @@ class AccountWatcher {
|
|
|
562
569
|
|
|
563
570
|
const newReposts = currentReposts - (prev.reposts ?? 0);
|
|
564
571
|
if (newReposts > 0) {
|
|
565
|
-
|
|
572
|
+
result.engagement.push(
|
|
566
573
|
`'${title}' got ${newReposts} repost${newReposts > 1 ? "s" : ""}!`
|
|
567
574
|
);
|
|
568
575
|
}
|
|
@@ -579,7 +586,7 @@ class AccountWatcher {
|
|
|
579
586
|
}
|
|
580
587
|
|
|
581
588
|
this.save();
|
|
582
|
-
return
|
|
589
|
+
return result;
|
|
583
590
|
}
|
|
584
591
|
}
|
|
585
592
|
|
|
@@ -764,12 +771,14 @@ export class SoundCloudWatcher {
|
|
|
764
771
|
private api: SoundCloudAPI;
|
|
765
772
|
private myTracksLimit: number;
|
|
766
773
|
private dormantDays: number;
|
|
774
|
+
private includeLinks: boolean;
|
|
767
775
|
private log: (...args: any[]) => void;
|
|
768
776
|
|
|
769
777
|
constructor(opts: SoundCloudWatcherConfig) {
|
|
770
778
|
this.log = opts.logger ?? console.log;
|
|
771
779
|
this.myTracksLimit = opts.myTracksLimit ?? 10;
|
|
772
780
|
this.dormantDays = opts.dormantDays ?? 90;
|
|
781
|
+
this.includeLinks = opts.includeLinks ?? true; // Default: include links
|
|
773
782
|
this.config = new Config(opts.clientId, opts.clientSecret, opts.username);
|
|
774
783
|
this.api = new SoundCloudAPI(this.config, this.log);
|
|
775
784
|
}
|
|
@@ -827,11 +836,24 @@ export class SoundCloudWatcher {
|
|
|
827
836
|
lines.push(`[${utcnow()}] Full check complete\n`);
|
|
828
837
|
|
|
829
838
|
lines.push("--- Account ---");
|
|
830
|
-
for (const
|
|
831
|
-
|
|
839
|
+
for (const fn of accountNotifs.followers) {
|
|
840
|
+
lines.push(...this.formatFollowerNotification(fn).map(l => ` ${l}`));
|
|
841
|
+
}
|
|
842
|
+
for (const e of accountNotifs.engagement) {
|
|
843
|
+
lines.push(` ${e}`);
|
|
844
|
+
}
|
|
845
|
+
if (!accountNotifs.followers.length && !accountNotifs.engagement.length) {
|
|
846
|
+
lines.push(" No updates");
|
|
847
|
+
}
|
|
832
848
|
|
|
833
849
|
lines.push("\n--- Artist Releases ---");
|
|
834
|
-
for (const r of releases)
|
|
850
|
+
for (const r of releases) {
|
|
851
|
+
if (this.includeLinks && r.url) {
|
|
852
|
+
lines.push(` ${r.artist}: ${r.title} - ${r.url}`);
|
|
853
|
+
} else {
|
|
854
|
+
lines.push(` ${r.artist}: ${r.title}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
835
857
|
if (!releases.length) lines.push(" No new releases");
|
|
836
858
|
|
|
837
859
|
lines.push(`\nAPI calls: ${this.api.calls}`);
|
|
@@ -868,6 +890,34 @@ export class SoundCloudWatcher {
|
|
|
868
890
|
return tracker.list();
|
|
869
891
|
}
|
|
870
892
|
|
|
893
|
+
private formatFollowerNotification(notif: FollowerNotification): string[] {
|
|
894
|
+
const lines: string[] = [];
|
|
895
|
+
const users = notif.users.slice(0, 5); // Max 5 users shown
|
|
896
|
+
const remaining = notif.users.length - users.length;
|
|
897
|
+
|
|
898
|
+
if (notif.type === 'new') {
|
|
899
|
+
lines.push(`New follower${notif.users.length > 1 ? 's' : ''}:`);
|
|
900
|
+
for (const u of users) {
|
|
901
|
+
if (this.includeLinks && u.permalink_url) {
|
|
902
|
+
lines.push(`- **${u.display_name}**: ${u.permalink_url}`);
|
|
903
|
+
} else {
|
|
904
|
+
lines.push(`- **${u.display_name}**`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
lines.push(`Lost follower${notif.users.length > 1 ? 's' : ''}:`);
|
|
909
|
+
for (const u of users) {
|
|
910
|
+
lines.push(`- ${u.display_name}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (remaining > 0) {
|
|
915
|
+
lines.push(` ...and ${remaining} more`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return lines;
|
|
919
|
+
}
|
|
920
|
+
|
|
871
921
|
async runCron(): Promise<string | null> {
|
|
872
922
|
const tokenErr = await this.ensureToken();
|
|
873
923
|
if (tokenErr) {
|
|
@@ -884,16 +934,31 @@ export class SoundCloudWatcher {
|
|
|
884
934
|
]);
|
|
885
935
|
|
|
886
936
|
const lines: string[] = [];
|
|
887
|
-
|
|
937
|
+
|
|
938
|
+
// Format follower notifications
|
|
939
|
+
const hasFollowerUpdates = accountNotifs.followers.length > 0;
|
|
940
|
+
const hasEngagement = accountNotifs.engagement.length > 0;
|
|
941
|
+
|
|
942
|
+
if (hasFollowerUpdates || hasEngagement) {
|
|
888
943
|
lines.push("**Account:**");
|
|
889
|
-
|
|
944
|
+
for (const fn of accountNotifs.followers) {
|
|
945
|
+
lines.push(...this.formatFollowerNotification(fn));
|
|
946
|
+
}
|
|
947
|
+
for (const e of accountNotifs.engagement) {
|
|
948
|
+
lines.push(`- ${e}`);
|
|
949
|
+
}
|
|
890
950
|
lines.push("");
|
|
891
951
|
}
|
|
952
|
+
|
|
892
953
|
if (releases.length) {
|
|
893
954
|
lines.push("**New Releases:**");
|
|
894
955
|
for (const r of releases) {
|
|
895
|
-
|
|
896
|
-
|
|
956
|
+
if (this.includeLinks && r.url) {
|
|
957
|
+
lines.push(`- **${r.artist}** dropped: ${r.title}`);
|
|
958
|
+
lines.push(` ${r.url}`);
|
|
959
|
+
} else {
|
|
960
|
+
lines.push(`- **${r.artist}** dropped: ${r.title}`);
|
|
961
|
+
}
|
|
897
962
|
}
|
|
898
963
|
lines.push("");
|
|
899
964
|
}
|