@akilles/soundcloud-watcher 2.0.4 → 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
@@ -2,214 +2,163 @@
2
2
 
3
3
  Monitor your SoundCloud account and track artist releases. Get notified when someone follows you, likes your tracks, or when artists you care about drop new music.
4
4
 
5
-
6
5
  ## Features
7
6
 
8
7
  - **Follower tracking** - See who followed you recently
9
8
  - **Track engagement** - Monitor who liked your tracks
10
9
  - **New releases** - Get notifications when tracked artists release new music
11
- - **Smart API usage** - Only fetches what changed, automatically skips dormant artists (configurable threshold)
10
+ - **Smart API usage** - Only fetches what changed, automatically skips dormant artists
12
11
  - **Rate limit handling** - Exponential backoff for API reliability
13
- - **Automatic background checking** - Configurable interval (default: 6 hours)
14
- - **Session-agnostic notifications** - Works with any OpenClaw session (Telegram, Discord, etc.)
15
12
 
16
13
  ## Prerequisites
17
14
 
18
15
  - OpenClaw gateway running
19
16
  - Node.js 22+ installed
20
- - SoundCloud API credentials ([get them here](https://soundcloud.com/you/apps))
17
+ - SoundCloud API credentials
21
18
 
22
19
  ## Quick Start
23
20
 
24
21
  ### 1. Install
25
22
 
26
23
  ```bash
27
- # From npm (recommended)
28
24
  openclaw plugins install @akilles/soundcloud-watcher
29
-
30
- # Or from source
31
- git clone https://github.com/wlinds/openclaw-soundcloud-watcher
32
- openclaw plugins install -l ./openclaw-soundcloud-watcher/openclaw-soundcloud-watcher
25
+ openclaw plugins enable soundcloud-watcher
26
+ openclaw gateway restart
33
27
  ```
34
28
 
35
-
36
29
  ### 2. Get SoundCloud Credentials
37
30
 
38
31
  1. Log into SoundCloud
39
32
  2. Go to [soundcloud.com/you/apps](https://soundcloud.com/you/apps)
40
33
  3. Click "Register a new application"
41
- 4. Fill in name and website
42
- 5. Copy your **Client ID** and **Client Secret** for next step
34
+ 4. Fill in name and website (any URL works)
35
+ 5. Copy your **Client ID** and **Client Secret**
43
36
 
44
37
  ### 3. Configure
45
38
 
46
- Run the setup command to see the configuration template:
39
+ Create the credentials file:
47
40
 
48
41
  ```bash
49
- /soundcloud-setup
50
- ```
51
-
52
- Then edit `~/.openclaw/openclaw.json` and paste your credentials:
53
-
54
- ```json
55
- {
56
- "plugins": {
57
- "enabled": true,
58
- "entries": {
59
- "soundcloud-watcher": {
60
- "enabled": true,
61
- "config": {
62
- "clientId": "YOUR_CLIENT_ID",
63
- "clientSecret": "YOUR_CLIENT_SECRET",
64
- "username": "your_soundcloud_username",
65
- "checkIntervalHours": 6,
66
- "myTracksLimit": 10,
67
- "dormantDays": 90,
68
- "sessionKey": "agent:main:main"
69
- }
70
- }
71
- }
72
- }
73
- }
42
+ nano ~/.openclaw/secrets/soundcloud.env
74
43
  ```
75
44
 
76
- ### 4. Restart & Verify
45
+ Add your credentials:
77
46
 
78
- ```bash
79
- openclaw gateway restart
80
- openclaw plugins list # Should show soundcloud-watcher
81
- /soundcloud-status # Should show your account info
82
47
  ```
48
+ SOUNDCLOUD_CLIENT_ID=your_client_id
49
+ SOUNDCLOUD_CLIENT_SECRET=your_client_secret
50
+ MY_USERNAME=your_soundcloud_username
83
51
 
84
- ### 5. Start Tracking your favorite artist
52
+ # Optional settings
53
+ INCLUDE_LINKS=true # Include URLs in notifications (default: true)
54
+ ```
55
+
56
+ ### 4. Restart & Verify
85
57
 
86
58
  ```bash
87
- /soundcloud-add lindstedt
88
- /soundcloud-add noisia
89
- /soundcloud-list
59
+ openclaw gateway restart
90
60
  ```
91
61
 
92
- Done! Updates arrive automatically every 6 hours.
62
+ Then in chat:
63
+ ```
64
+ /soundcloud-setup # Should show "Already configured!"
65
+ /soundcloud-status # Should show your account info
66
+ ```
93
67
 
94
68
  ## Commands
95
69
 
96
70
  | Command | Description |
97
71
  |---------|-------------|
98
- | `/soundcloud-setup` | Interactive setup guide with config status |
72
+ | `/soundcloud-setup` | Show setup instructions and config status |
99
73
  | `/soundcloud-status` | Show tracking status and account info |
100
- | `/soundcloud-check` | Run immediate check (don't wait for interval) |
74
+ | `/soundcloud-check` | Run immediate check (verbose output) |
75
+ | `/soundcloud-cron` | Run check for automation (silent if no updates) |
101
76
  | `/soundcloud-add <username>` | Track artist(s) - space-separated |
102
- | `/soundcloud-remove <username>` | Untrack artist |
77
+ | `/soundcloud-remove <username>` | Stop tracking an artist |
103
78
  | `/soundcloud-list` | List all tracked artists |
104
79
 
105
- ## Configuration Options
106
-
107
- All options in `~/.openclaw/openclaw.json` under `plugins.entries.soundcloud-watcher.config`:
108
-
109
- | Option | Type | Required | Default | Description |
110
- |--------|------|----------|---------|-------------|
111
- | `enabled` | boolean | No | true | Enable/disable watcher |
112
- | `clientId` | string | Yes | - | SoundCloud API Client ID |
113
- | `clientSecret` | string | Yes | - | SoundCloud API Client Secret |
114
- | `username` | string | Yes | - | Your SoundCloud username |
115
- | `checkIntervalHours` | number | No | 6 | Hours between automatic checks |
116
- | `myTracksLimit` | number | No | 10 | Number of your tracks to monitor |
117
- | `dormantDays` | number | No | 90 | Days before artist is considered dormant |
118
- | `sessionKey` | string | No | `agent:main:main` | OpenClaw session for notifications |
80
+ ## Automated Checking
119
81
 
120
- ## Architecture
82
+ The plugin responds to commands but doesn't auto-poll. Set up a cron job for automatic notifications:
121
83
 
122
- The plugin consists of three main components:
84
+ ```bash
85
+ openclaw cron add --name "soundcloud-check" \
86
+ --every 6h \
87
+ --isolated \
88
+ --message "Run /soundcloud-cron and forward any updates to me on Telegram."
89
+ ```
123
90
 
124
- - **Plugin entry** ([openclaw-soundcloud-watcher/index.ts](openclaw-soundcloud-watcher/index.ts)) - Manages lifecycle, spawns watcher process
125
- - **Watcher** ([openclaw-soundcloud-watcher/soundcloud_watcher.ts](openclaw-soundcloud-watcher/soundcloud_watcher.ts)) - Pure TypeScript implementation of monitoring logic
126
- - **Manifest** ([openclaw-soundcloud-watcher/openclaw.plugin.json](openclaw-soundcloud-watcher/openclaw.plugin.json)) - Configuration schema and plugin metadata
91
+ Uses `/soundcloud-cron` which:
92
+ - Returns updates only (silent if nothing new)
93
+ - Logs errors but doesn't spam on config issues
127
94
 
128
- ### File Locations
95
+ **Alternative:** Add to your `HEARTBEAT.md`:
96
+ ```markdown
97
+ - [ ] Run /soundcloud-cron if not checked in last 6 hours
98
+ ```
129
99
 
130
- After installation:
100
+ ## File Locations
131
101
 
132
- - **Plugin code:** `~/.openclaw/extensions/soundcloud-watcher/`
133
- - **Config:** `~/.openclaw/openclaw.json`
134
- - **Credentials:** `~/.openclaw/secrets/soundcloud.env`
135
- - **Account data:** `~/.openclaw/data/soundcloud_tracking.json`
136
- - **Artist data:** `~/.openclaw/data/artists.json`
137
- - **Backoff state:** `~/.openclaw/soundcloud_backoff.json`
102
+ | File | Purpose |
103
+ |------|---------|
104
+ | `~/.openclaw/secrets/soundcloud.env` | Your API credentials |
105
+ | `~/.openclaw/data/artists.json` | Tracked artists data |
106
+ | `~/.openclaw/data/soundcloud_tracking.json` | Your account tracking data |
138
107
 
139
108
  ## Troubleshooting
140
109
 
141
- ### Plugin not loading
110
+ ### "Not configured" error
142
111
 
143
- ```bash
144
- openclaw plugins list
145
- openclaw gateway logs
146
- ```
112
+ Check your credentials file exists and has correct format:
147
113
 
148
- Check that:
149
- - Plugin shows as `enabled: true` in list
150
- - Gateway logs don't show errors
151
-
152
- Verify plugin directory exists:
153
114
  ```bash
154
- ls -la ~/.openclaw/extensions/soundcloud-watcher/
115
+ cat ~/.openclaw/secrets/soundcloud.env
155
116
  ```
156
117
 
157
- ### API rate limits
158
-
159
- If you hit rate limits:
160
- 1. Increase `checkIntervalHours` in config (default: 6)
161
- 2. Increase `dormantDays` to skip inactive artists sooner (default: 90)
162
- 3. Check SoundCloud API status
163
-
164
- ### No notifications
118
+ Should contain:
119
+ ```
120
+ SOUNDCLOUD_CLIENT_ID=...
121
+ SOUNDCLOUD_CLIENT_SECRET=...
122
+ MY_USERNAME=...
123
+ ```
165
124
 
166
- Check gateway session key in the plugin config (default is `agent:main:main`).
125
+ ### Plugin not loading
167
126
 
168
- Verify gateway is running:
169
127
  ```bash
170
- openclaw gateway status
128
+ openclaw plugins list
171
129
  ```
172
130
 
173
- ### Setup help
174
-
175
- Run `/soundcloud-setup` for detailed instructions with current config status.
131
+ Should show `soundcloud-watcher` as `loaded`. If `disabled`:
176
132
 
177
- ## Updating
178
-
179
- If installed via symlink (`-l`):
180
133
  ```bash
181
- cd /path/to/openclaw-soundcloud-watcher
182
- git pull
134
+ openclaw plugins enable soundcloud-watcher
183
135
  openclaw gateway restart
184
136
  ```
185
137
 
186
- If installed from npm:
187
- ```bash
188
- openclaw plugins install @akilles/soundcloud-watcher # Gets latest
189
- openclaw gateway restart
190
- ```
138
+ ### API rate limits
139
+
140
+ The plugin handles rate limits automatically with exponential backoff. If issues persist, wait a few minutes and try again.
191
141
 
192
142
  ## Uninstalling
193
143
 
194
144
  ```bash
195
145
  openclaw plugins disable soundcloud-watcher
196
- openclaw plugins uninstall soundcloud-watcher
146
+ rm -rf ~/.openclaw/extensions/soundcloud-watcher
197
147
  ```
198
148
 
199
- Clean up data (optional):
149
+ Optionally remove data:
200
150
  ```bash
201
- rm -rf ~/.openclaw/data/soundcloud_tracking.json
202
- rm -rf ~/.openclaw/data/artists.json
203
- rm -rf ~/.openclaw/secrets/soundcloud.env
204
- rm -rf ~/.openclaw/soundcloud_backoff.json
151
+ rm ~/.openclaw/secrets/soundcloud.env
152
+ rm ~/.openclaw/data/artists.json
153
+ rm ~/.openclaw/data/soundcloud_tracking.json
205
154
  ```
206
155
 
207
- ## Support
156
+ ## Links
208
157
 
209
158
  - **GitHub:** https://github.com/wlinds/openclaw-soundcloud-watcher
210
- - **Issues:** https://github.com/wlinds/openclaw-soundcloud-watcher/issues
159
+ - **npm:** https://www.npmjs.com/package/@akilles/soundcloud-watcher
211
160
  - **OpenClaw Docs:** https://docs.openclaw.ai/plugin
212
161
 
213
162
  ## License
214
163
 
215
- MIT - See [LICENSE](LICENSE) for details
164
+ MIT
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;
@@ -178,5 +181,29 @@ MY_USERNAME=your_soundcloud_username
178
181
  },
179
182
  });
180
183
 
184
+ api.registerCommand({
185
+ name: 'soundcloud-cron',
186
+ description: 'Run check for cron (only outputs if there are updates)',
187
+ handler: async () => {
188
+ const w = getWatcher();
189
+ if (!w) {
190
+ (logger.warn ?? console.warn).call(logger, 'SoundCloud cron: not configured');
191
+ return { text: '' }; // Silent fail for cron
192
+ }
193
+ try {
194
+ const result = await w.runCron();
195
+ if (result) {
196
+ return { text: result };
197
+ }
198
+ // No updates - return empty (silent success)
199
+ (logger.debug ?? console.log).call(logger, 'SoundCloud cron: no updates');
200
+ return { text: '' };
201
+ } catch (e) {
202
+ (logger.error ?? console.error).call(logger, 'SoundCloud cron error:', e);
203
+ return { text: `SoundCloud check failed: ${e}` };
204
+ }
205
+ },
206
+ });
207
+
181
208
  (logger.info ?? console.log).call(logger, 'SoundCloud Watcher plugin loaded');
182
209
  }
@@ -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.4",
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
  }