- TypeScript 97%
- Shell 2%
- JavaScript 1%
whereditgo-ranger #
Bluesky bot that posts a random track and its cover art from the Navidrome server at radio.whereditgo.diamonds every five days at noon Eastern. It also answers mentions with a track, once per person every 6 hours.
How it works #
- An EventBridge Scheduler schedule invokes the Lambda hourly from noon to 5pm Eastern, every day. It follows daylight saving.
- The Lambda exits right away unless today is a posting day. Posting days are counted in 5-day steps from
firstPostDate. - It records the attempt in DynamoDB, then asks Navidrome's Subsonic API for 50 random tracks, using a dedicated service account.
- It claims the first track that isn't on cooldown, with a conditional write to DynamoDB.
- It fetches the cover art, logs in to Bluesky with an app password, uploads the image, and publishes the post.
Settings live in bin/whereditgo-ranger.ts.
Retries #
The noon run is the first attempt. If it fails for any reason, the 1pm to 5pm runs retry it, for 5 retries in all. Once a day has posted, its later runs do nothing. If all six attempts fail, that post is skipped and the next one goes out on the next posting day.
Each posting day has an item in the PostRuns table, keyed by date (runDate):
| Status | Meaning |
|---|---|
attempting |
An attempt is running, or it crashed before recording a result |
posted |
Done. postUri and postText hold the post |
failed |
The last attempt failed and retries remain. lastError holds the reason |
abandoned |
All attempts failed. lastError holds the last reason |
Each retry picks a new random track. No alert goes out on failure, so check this table or the Lambda's logs.
Repeats #
A track counts as a repeat if the bot has posted it in the last 365 days (cooldownDays). Entries in the PostHistory table expire after that, which makes the track eligible again. Tracks are matched on normalized artist and title rather than Navidrome's ID, because IDs change when a file moves.
If no eligible track turns up in three samples, the attempt fails.
Cover art #
Images are requested at 1000px and step down to 600px and 300px to fit Bluesky's 1MB limit. Every post carries cover art, so tracks Navidrome lists with no art are skipped. If the art can't be fetched, or is too large at every size, that attempt fails.
Posts don't link to the audio, and the bot doesn't create Navidrome share links.
Post format #
Scheduled posts and replies look the same: the cover art, plus this text.
Title by Artist
Replying to pings #
A second Lambda checks the bot's notifications every 6 hours. It replies in the thread to anyone who has @-mentioned it, in a new post or a reply, with a random track and its cover art. A reply to one of the bot's posts that doesn't name the bot isn't a mention.
- Once per person every 6 hours: someone who mentions the bot several times between checks gets one reply, to their newest mention. The
ReplyHistorytable tracks each person's last reply and answered mention, keyed by DID so that changing handles doesn't reset it. A mention that has been answered is never answered again. - Repeats: reply tracks skip the 365-day rule and aren't added to
PostHistory, so pings can't use up tracks for scheduled posts. A reply can repeat a recent track. - Failures: if a reply fails, the person's entry is removed and the next run tries again. Mentions stay eligible for 3 days, after which they're ignored.
- Limit: each run sends at most 20 replies. Anyone beyond that waits for the next run.
- Read state: the bot doesn't mark notifications as read, so opening the account in the app doesn't affect it.
The first check runs 6 hours after deployment and answers any pings from the previous 3 days.
Setup #
1. Create the accounts #
- Navidrome: create a regular, non-admin user for the bot.
- Bluesky: in the bot account, go to Settings → Privacy and Security → App Passwords and create one. Don't use the account's main password.
2. Store the credentials #
Run these with your own values. CDK doesn't manage them, because CloudFormation can't create SecureStrings.
aws ssm put-parameter --region us-east-1 --type SecureString --name /whereditgo-ranger/navidrome/username --value '<username>'
aws ssm put-parameter --region us-east-1 --type SecureString --name /whereditgo-ranger/navidrome/password --value '<password>'
aws ssm put-parameter --region us-east-1 --type SecureString --name /whereditgo-ranger/bluesky/identifier --value '<handle>'
aws ssm put-parameter --region us-east-1 --type SecureString --name /whereditgo-ranger/bluesky/app-password --value '<app-password>'
3. Deploy #
npm install
npm test
npx cdk deploy
Testing a deployment #
The quickest way is scripts/post-now.sh. Run it alone to make a real post, or with --dry-run to preview one. The commands below do the same thing by hand.
Replace <FunctionName> with the FunctionName stack output.
A dry run checks the Navidrome login and cover art, and returns a sample post. It publishes nothing and records nothing, and it doesn't use the Bluesky credentials:
aws lambda invoke --region us-east-1 --function-name <FunctionName> --cli-binary-format raw-in-base64-out --payload '{"dryRun":true}' /dev/stdout
This makes a real post right away, on any day. It doesn't count toward the day's attempts or change the cycle, but the track does go on cooldown:
aws lambda invoke --region us-east-1 --function-name <FunctionName> --cli-binary-format raw-in-base64-out --payload '{"force":true}' /dev/stdout
To test replies, mention the bot from another account, then run the check now, replacing <ReplyFunctionName> with that stack output. This sends real replies to any unanswered mentions from the last 3 days, and that account can't get another reply for 6 hours:
aws lambda invoke --region us-east-1 --function-name <ReplyFunctionName> /dev/stdout