I’m not dead yet, I’m just resting! I took a couple weeks to tour Japan with my daughter and then a couple more weeks to get over whatever I caught at the end of our trip (I genuinely suspect it was the terrible throat variation of Covid going around. It wiped me out).
I have updates though!
Spotlighting Tahoe
First of all, I installed the Tahoe developer beta on my Mac, and I immediately started playing with Apple’s updated Spotlight. If I had to bet money, I would never have put any down on being able to replace Raycast with it, but that’s exactly what I’ve done. I’ll have several posts on the hows and whys of this in the future.
Photos with a Lightbox
I took many photos in Japan. Not nearly enough, as it turns out, but many. Before I left for Japan, I started whipping together an Astro site that I could post them to, imagining I’d live-post from Japan, but that didn’t happen. In fact, I just now have the site to a workable state I’m happy with. It’s at Photos and Words. I haven’t started adding my Japan trip photos yet though – stay tuned!
The combination of Tailscale and my GL-AXT1800 / Slate AX - GL.iNet travel router was a huge win for me in Japan. The travel router is nice – it can connect to any wifi source so I was able to get it on the hotel networks and then all our devices could just connect to it. Even better, it supports Tailscale so I could use an exit node that is allowed to SSH into servers I help manage and web apps I need for paid work.
More to Come
Each of these probably deserves some coverage on its own, which is my plan. I’m really looking forward into coalescing my thoughts on Spotlight as a Raycast sherlocker, because I know many people will not believe it possible.
I made a change yesterday to my /now page to make the Podcasts section look nicer by including podcast artwork next to the episode entries. I think it adds visual interest, and (most importantly) it was fun to update my recent podcast history script to include it.
This change was partially spurred by me switching from Overcast as my podcast app to Apple Podcasts due to a couple Overcast bugs that were annoying me. However, switching podcast apps did mean updating my /now page Podcast episodes would be a bit more manual, due to Apple Podcasts not saving the history in a way that I can retrieve via automation. I started looking at using the iTunes API to grab episode information, and while doing so, I figured I may as well perform the simple chore of also grabbing the artwork along with the other episode information.
But then Marco updated Overcast a couple days ago and it fixed some of the bugs that were bugging me, so I bugged right back out of Apple Podcasts land and back to Overcast.1 At this point, I could have just gone back to my Recent Overcast History Raycast script command with no changes, and no one would have ever noticed based on what appears on my /now page, but the thought of showing podcast artwork as part of my recent listening history stuck with me. I decided I had to do it.
Understanding how this works requires a little background on other parts of this script. Rather than bore you to death with ALL the details, I’ll just give you some relevant code snippets and then give full python module code dumps at the end. First though, here’s a before and after comparison of my recent podcast listening history.
Then:
Now:
Podcast Listening History Overview
Basically the way I automate grabbing my recent podcast listening history for my /now page goes like this:
Log into my Overcast account and download my Overcast history OPML file.
Parse the OPML file for all podcast-episode elements with the played attribute set to 1.
Filter these down to a specfic number of days worth, order them with most recently listened first, and then filter them again so that only the last 10 of those are returned.
For each episode in this list, load the overcast episode details page and parse the URL of the podcast artwork image. Download it and shove it in a specific folder. Then create the markdown links (including images) for the episode for display on the /now page and append it to the full list of episode links.
Put this list of markdown links on the clipboard.
Run a Retrobatch action to scale the artwork way down and put it in the correct folder in the website source for site compilation.
All the above happens automatically when I run the script. I have it set up as a Raycast script command, so I trigger it to happen. I could have it update itself on a specific basis and automatically update, compile, and push the site, but I don’t. But that’s a logical series of next steps for anyone who wants a fully automated experience.
The Raycast Script Command
Here’s the Raycast script command. It’s called Recent Overcast History. Note that any Raycast script command can be run outside of Raycast like any other script of its kind (bash, in this case), but I do call this through Raycast.
Recent Overcast History has one dropdown parameter I need to choose: “Normal” or “Force”. I have this because if it’s been less than 2 days, I don’t redownload the OPML file unless I choose “Force”. One nice thing about Raycast is it always remembers what your last choice was and defaults to that.
The Python Scripts
Recent Overcast History’s first action is to run scottwillsey-overcast-history/main.py, which is responsible for initiating the series of steps I outlined above.
main.py
1
#!/Users/scott/Scripts/python/venv/bin/python
2
import argparse
3
import os
4
from datetime import datetime, timedelta
5
import constants as const
6
from oc_history import load_oc_history
7
from oc_opml_parse import oc_opml_parse
8
9
p = argparse.ArgumentParser()
10
p.add_argument('-f','--force',action='store_true',help='Force local OPML file parsing')
print(f'OPML file is older than {const.OPML_AGE_LIMIT_DAYS} days or doesn\'t exist. Downloading new data...')
32
history_was_loaded =load_oc_history()
33
else:
34
print(f'OPML file is less than {const.OPML_AGE_LIMIT_DAYS} days old. Skipping download.')
35
36
if history_was_loaded or args.force:
37
print('Parsing OPML file...')
38
ifoc_opml_parse():
39
print('Done!')
40
else:
41
print('You have to update your podcast list manually.')
42
else:
43
print('No new Overcast history generated.')
44
45
46
47
if__name__=="__main__":
48
main()
I’m not going to go through all the scripts like the login script and the downloading of the OPML file, but the OPML parsing script is interesting so you can see how I’m finding the episode history I want, getting podcast artwork for it, and then creating the markdown for each episode that goes on the /now page.
oc_opml_parse.py
1
import pyperclip
2
import xml.etree.ElementTreeasET
3
import constants as const
4
from datetime import datetime, timezone, timedelta
5
from oc_artwork import load_episode_artwork
6
7
deffind_podcast_name(root,episode_id):
8
for podcast in root.findall(".//outline[@type='rss']"):
9
for ep in podcast.findall("outline[@type='podcast-episode']"):
All the real fun happens in the function oc_opml_parse(), which is called by main.py. First it tries to open the OPML file and then uses the xml.etree.ElementTree module to get the XML model of the data.
1
defoc_opml_parse():
2
withopen(const.OPML_FILE_PATH,'r')as f:
3
content = f.read()
4
try:
5
withopen(const.OPML_FILE_PATH,'r')as f:
6
content = f.read()
7
exceptFileNotFoundError:
8
print(f"File not found: {const.OPML_FILE_PATH}")
9
returnNone
10
11
root =ET.fromstring(content)
Next, it parses that for the list of episodes and starts winnowing them down to played episodes, ordered by most recent first, covering a specific timespan, and then returns the first 10 episodes on that list.
Finally, it creates the markdown for all of the episodes in the list, including loading the Overcast episode page for each episode and finding the podcast artwork from it.
print(f'Failed to download artwork. Status code: {art.status_code}')
41
else:
42
print('No artwork URL found on episode page.')
43
else:
44
print(f'Failed to download Overcast episode page. Status code: {r.status_code}')
45
46
returnNone
Because the image URL for the artwork always contains https://public.overcast-cdn.com/art/, it’s always a simple thing to find it with a regular expression and then download that file. I name the artwork files as the Overcast episode ID with a UUID appended. I save those to a folder for processing by Retrobatch.
The Retrobatch Script
Remember how the script command runs the Python script and then runs a Retrobatch action (script command included below for memory-jogging purposes)?
It grabs images from /Users/scott/Scripts/scottwillsey/scottwillsey-overcast-history/images, where the Python script saved them, resizes them to 250 pixels wide, with corresponding correct height, and saves them to my website source’s oc_artwork directory for inclusion when the site is compiled.
After that, the script command deletes the source images from /Users/scott/Scripts/scottwillsey/scottwillsey-overcast-history/images.
The Markdown
The result of ALL OF THIS is Markdown that looks like this:
1
[](https://overcast.fm/+BL5ZgWiWVU)[Click Here – Crypto in Kentucky: The next extraction](https://overcast.fm/+BL5ZgWiWVU)
2
The 404 Media Podcast (Premium Feed) – Subscriber Comment Show: Don’t Read Our Emails With Your Kids, Maybe
3
[](https://overcast.fm/+BFs16bV2bY)[Uncanny Valley | WIRED – Is Elon Musk Really Stepping Back from DOGE?](https://overcast.fm/+BFs16bV2bY)
4
[](https://overcast.fm/+B7NDTZ7xw)[The Talk Show With John Gruber – 421: ‘The Ratchet of Flippancy’, With Craig Mod](https://overcast.fm/+B7NDTZ7xw)
5
[](https://overcast.fm/+De1kB24UY)[The Rebound – 546: Dave And/Or Buster’s](https://overcast.fm/+De1kB24UY)
6
Six Colors – CarPlay Ultra and legal localization (6C Podcast)
7
AppStories+ – Airbnb 2025 Summer Release: An Interview with Jud Coplan and Teo Connor
8
[](https://overcast.fm/+JGaBVhHcw)[Programming By Stealth – PBS 179 of X — Introducing Liquid Templates in GitHub Pages with Jekyll](https://overcast.fm/+JGaBVhHcw)
9
[](https://overcast.fm/+5Sl_VEs2k)[Risky Bulletin – Between Two Nerds: Why hackers and spies don’t mix](https://overcast.fm/+5Sl_VEs2k)
You may have noticed, if you’re the type of person who just loves staring at and parsing markdown listings, that not all of these actually link to the Overcast episode. I remove the links for paid podcast episodes. I don’t pay for a lot of podcasts, but I do pay for some.
And that’s it! It all happens instantly and automatically when I fire up the script command from Raycast!
Here’s a use case for AI that I really like: a Japanese to English translator that gives me a translation and breakdown of the kanji (Chinese characters) in a Japanese phrase or passage. The reason AI is really good for this as opposed to Japanese apps that let you paste in phrases or do text detection in images is that you get translation, the ability to specify how you want the results returned, the image text detection, AND the ability to ask follow-up questions.
I created both a Raycast command and a Raycast AI chat preset with instructions on translation from Japanese to English.
The Raycast AI Command:
The Raycast AI Chat Preset:
The command opens in Raycast Quick AI view and uses the contents of the clipboard as the source (including screenshots or photos). The chat preset is for the separate AI chat window and I can paste or attach whatever I like as the source.
Here’s an example use case:
I took a screenshot of an easy Japanese lesson from JapanesePod101.com and gave it to the chat preset to translate.
やめてよ。「野沢」か、「菜味」って呼んでって何度も言っているでしょう。
Here’s the full translation result, including a follow-up question of mine as to why it used one specific word for “many times” instead of another.
Notification: Preset: J to E translator
---
Scott Willsey:
---
Raycast AI: Okay, here is the breakdown and translation:
**English Translation:**
Stop it! I've told you so many times to call me "Nozawa" or "Nami", haven't I?
**Japanese Transcription (Kana):**
やめてよ。「のざわ」か、「なみ」ってよんでってなんどもいっているでしょう。
(Yamete yo. "Nozawa" ka, "Nami" tte yonde tte nando mo itte iru deshou.)
**Kanji Breakdown:**
| Kanji | Reading(s) | Meaning | Compound Word(s) | Compound Reading | Compound Meaning |
| 呼 | yo(bu), ko | call, invite, breathe out | 呼んで | yonde | Call (te-form of 呼ぶ) |
| 何 | nan, nani | what, how many | 何度も | nando mo | many times, repeatedly |
| 度 | do, tabi | degrees, occurrence, time | 何度も | nando mo | many times, repeatedly |
| 言 | i(u), koto | say, word, speech, statement | 言っている | itte iru | are saying, have said |
*\*Note: 菜味 (Nami) is one possible reading for this name. Other readings exist.*
---
Scott Willsey: why 何度 intead of 何回 ?
---
Raycast AI: That's a great question! While both 何度 (nando) and 何回 (nankai) translate to "how many times" or "how often," there's a subtle difference in nuance and usage.
1.**何回 (nankai): Focus on Countable Occurrences**
* This tends to focus more on the specific, countable *number* of times an action happened or will happen. It's often used when you could potentially answer with a precise number (even if the speaker doesn't know it).
* Think of it like counting discrete events.
* Example: 「この映画、**何回**見ましたか?」 (Kono eiga, **nankai** mimashita ka?) - "How many times have you seen this movie?" (You could answer: 3 times, 5 times, etc.)
2.**何度 (nando): Focus on Repetition/Frequency (Slightly More Abstract/Emphatic)**
* This often emphasizes the *repetition* or *frequency* itself, rather than just the exact count. It can feel slightly more abstract or carry a bit more emotional weight (like surprise, exasperation, or gratitude for repeated actions).
* It's very common in the pattern **何度も (nando mo)**, meaning "many times," "again and again," or "repeatedly."
* Example: 「**何度も**すみません。」 ( **Nando mo** sumimasen.) - "Sorry [for bothering you] so many times." (Focus is on the repeated nature of the bother).
* Example: 「**何度**言ったらわかるの?」 ( **Nando** ittara wakaru no?) - "How many times do I have to tell you?" (Expresses frustration at the repetition needed).
Here, **何度も (nando mo)** is used. This emphasizes the *repeated nature* of the speaker telling the listener how to address them. It conveys a sense of "I've told you this *again and again*." The focus isn't necessarily on *exactly* how many times (was it 5 times? 10 times?), but on the fact that it has happened *repeatedly*, implying frustration.
While you *could* technically say 「何回も言っているでしょう」 (nankai mo itte iru deshou), **何度も** feels slightly more natural and common in this specific context of expressing exasperation about repeated instructions or warnings. It highlights the *frequency* and the speaker's feeling about it.
Not only does this give me a full breakdown of the kanji, including their compound and individual meanings, but it lets me ask questions about specific parts of the sentence.
Bear in mind I lived in Japan for 8 years and I’m going to know when an LLM gives me something bogus or tries to lead me astray. But this is good for reminding me how specific conjugations work or the differences in terms that I might not understand.
I was a kid when I lived in Japan, and I never formally studied Japanese. My learning was based on intuitive understanding as I played with other kids, watched TV, and spent time listening to people around me talking. Inevitably I didn’t learn some nuances or made incorrect assumptions that then became part of my understanding of Japanese. That’s why use cases like this, with a custom kanji breakdown instruction and the ability to ask questions about the translation or the source phrase, are super handy for me.
Here’s what my current Music Reviews page looks like. The goal is to start adding them in a way that creates a diverse mix of music while I build up the number of reviews available.
IndieWeb is where the fun is… if you like the random stuff I throw on my site, think about how to make your own or what you want it to be to try to make life just a little bit happier for a minute or two for anyone who stumbles across you. We could all use some good stuff in life right now.
When I added a Reviews page, I wanted to be able to have individual reviews show up in my site search. Due to some complexities I won’t go into of how Pagefind indexes things and how I optimize my images using the Astro Image component, this was easier said than done – until I remembered the good old HTML hidden global attribute. Then I just made a hidden span and populated it with the image alt text.
I’ve spoken a couple times about my Sessions Raycast script command, both when I originally set it up, and then again when I updated it to use Moom as my window manager. Well, I’m back to using Raycast as my window manager, and I also use a Raycast focus mode for my podcast session. So here’s your updated Session Raycast script command, 2025 edition.
You may recall that the purpose of this script command is to just set up my environment in a way that’s suitable for a specific task. It always closes all apps, then opens some core apps, and then opens other apps specific to the session type.1 It also sets audio output and input based on the session type, arranges app windows in a specific way, and either sets Coffee for Raycast to Caffinate mode (keep the screen on) or Decaffinate mode (let it sleep when it wants).
When I run the Sessions script command in Raycast, I just choose the session type. The nice thing about Raycast dropdowns is they pre-select the option you chose last time. In the case of the Session script command, that’s not as convenient as it is for some other script commands, but that’s life.
Anyway, if you have any questions about this script command or Raycast script commands in general, you know how to find me (hint: Mastodon or Bluesky).
Footnotes
I should see if there’s a way to close all apps that aren’t in a list, instead of closing all apps and then re-opening all apps that are in a list. ↩
Last week or so, I started writing a blog post as I sometimes do, this one pertaining to my Automation Workflow for Media Reviews. Unfortunately, I wanted to preview it as I went along, so I copied it in progress to my git main branch of the local copy of the website.
You can see where this is going.
Yes, I updated something else on the website and published it, INCLUDING the partial draft of the blog post I was working on. This wasn’t a super huge deal, except that I use EchoFeed to automatically post to Bluesky and Mastodon whenever I post something new on the site.
Sigh.
The good news is that this finally pushed me to add drafts functionality to my site, so that I could have drafts render when running locally in development mode, but not actually get written when doing a site build. It’s a good, basic feature to have.
I’ve always wanted to be a writer, and I’ve always wanted to create drafts in Astro that won’t get published until I want them to.
10
11
[Now I can!](https://jacurtis.com/notes/astro-draft-posts/)
One more thing though – none of this keeps the post page itself from being rendered during a build. It just keeps anything from linking to it or showing it in a list of posts. This means that it will show up in your RSS feed unless you edit your RSS template to also filter it out.