@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 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;
@@ -2,7 +2,7 @@
2
2
  "id": "soundcloud-watcher",
3
3
  "name": "SoundCloud Watcher",
4
4
  "description": "Monitor your SoundCloud account and track artist releases",
5
- "version": "2.0.4",
5
+ "version": "2.1.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akilles/soundcloud-watcher",
3
- "version": "2.0.5",
3
+ "version": "2.1.0",
4
4
  "description": "OpenClaw plugin to monitor SoundCloud account and track artist releases",
5
5
  "main": "index.ts",
6
6
  "openclaw": {
@@ -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: f.permalink ?? f.username ?? "unknown",
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<string[]> {
446
- const notifications: string[] = [];
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 ["Failed to resolve SoundCloud user"];
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 notifications;
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.display_name);
496
+ .map(([, f]) => f);
483
497
  const lostFollowers = Object.entries(stored)
484
498
  .filter(([uid]) => !currentFollowers[uid])
485
- .map(([, f]) => f.display_name);
499
+ .map(([, f]) => f);
486
500
 
487
501
  if (newFollowers.length) {
488
- let names = newFollowers.slice(0, 3).join(", ");
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
- const names = lostFollowers.slice(0, 3).join(", ");
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
- notifications.push(`**${names}** liked '${title}'`);
560
+ result.engagement.push(`**${names}** liked '${title}'`);
554
561
  }
555
562
  if (unlikerNames.length) {
556
563
  const names = unlikerNames.slice(0, 3).join(", ");
557
- notifications.push(`${names} unliked '${title}'`);
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
- notifications.push(
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 notifications;
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 n of accountNotifs) lines.push(` ${n}`);
831
- if (!accountNotifs.length) lines.push(" No updates");
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) lines.push(` ${r.artist}: ${r.title}`);
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
- if (accountNotifs.length) {
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
- lines.push(...accountNotifs.map((n) => `- ${n}`));
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
- lines.push(`- **${r.artist}** dropped: ${r.title}`);
896
- lines.push(` ${r.url}`);
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
  }