Show HN: I Made an iOS Podcast Player with Racket
defn.io
defn.io
For example, some podcasts I don't want to miss an episode and I want them all downloaded. Other podcasts I only check in on occasionally and would only want the latest episode to be kept on device.
I subscribe to a lot of podcast and downloading and keeping every episode is going to eat up a lot of storage.
Besides that, love the simplicity of it. Well done!
If you've checked out the thing being showhn enough to have a feature request, you're doing Show HN right.
I want to be able to categorize content. I want a category of podcast to work to, work out to, go to sleep to, or to simply sit a learn. I want different categories of music. I want to be able to set a group of content on YouTube that I will watch everything on, and one that I can sort through and pick the few videos I want to watch.
Given the value of that data in just my sorting and prioritizing of content, I don't know why I don't have the tools so that data can be harvested and sold.
I think it will lead to better content.
1. You build Racket for iOS in an interpreted mode (regular Racket works a bit like a JIT in that it needs to load code into memory at runtime, which is a no-go on iOS)
2. You link to it as a regular static library.
3. You call Racket's C API[2] to load and run Racket code.
Here's[3] a somewhat outdated video I did a while back on using that library for Mac app development. Many of the things in this video are now much simpler/more straightforward, but maybe it serves to give you an idea. I also have some source-available Mac apps built this way that you can take a look at[4, 5].
[1]: https://github.com/Bogdanp/Noise?tab=readme-ov-file#usage
[2]: https://docs.racket-lang.org/inside/cs.html
[3]: https://www.youtube.com/watch?v=aTvU0j4hBR0
https://docs.racket-lang.org/inside/ios-cross-compilation.ht...
I’ve been a Pocket Casts users since I can remember. I’ll +1 the CarPlay support that you mention was already on your list. The other, which they’ve recently introduced, is to disable Lock Screen scrubbing. Been a life saver!
Looks great though! I’ll keep an eye on the change long as it evolves.
I was so happy I happened to read through their recent changelog and caught that. WAY too many times I accidentally swiped along the scrub bar and put myself at a random place in a 4 hour podcast episode and had to spend 5 minutes trying to figure out where I was.
[1]: https://siftrss.com/
EDIT: Oh, I just realized you might be referring to my blog, in which case I _can_ take credit for that :P. Thanks again!
The app privacy label says it tracks diagnostics.
I will never understand why developers believe that they are entitled to information from devices that they do not own.
string::1: bytes->jsexpr:
bad input starting #"error code: 502" context..:
.../syntax/readerr.rkt:15:2: -raise- read-error
.../private/arrow-val-first.rkt:486:18
.../private/backend.rkt:45:9: get-trending-podcasts
.../noise-serde-lib/backend.rkt:69:22edit: oh right,
> I am still debating whether or not I want to make the app itself source available, like I've done for Franz and Remember. Maybe if there's interest.
I tried looking through your blog but couldn’t find anything except the 40 minute YouTube video for your other app. It sounds like both the UI and the audio-related code are in Swift? What code ends up actually being in Racket then?
Do features like these, when implemented in Racket, consume more resources (battery, CPU, etc.) than if they were implemented using the native API equivalents (e.g., NSURLSession or whichever is more applicable)?
In the larger context of cross-platform apps with a common core written in a non-native programming stack, I often wonder this about network and disk I/O management. I understand using the native APIs for other "I/O" like UI, hardware interfaces (bluetooth, accelerometer, etc.), because they often don't have an equivalent API in the programming stack used to implement the common core.
As far as I know, Capacitor wraps over the native APIs.
Have you used https://github.com/Bogdanp/Noise? I assume serde would take up memory on both sides (Racket and Swift).
Sorry for a barrage of questions. I am quite curious about how all of this works. I am mulling over using OCaml for what you've used Racket for.
Yes, the app uses Noise under the hood, and the way to think about it is a request-response model[1]. Swift makes an async request to the Racket backend, it constructs some data (either by querying SQLite, or making a request to the backend, etc.) and returns a response. If it doesn't retain any of the data, then it gets GC'd. The ser/de is relatively low overhead -- if you try the app and go to Settings -> Support and take a look at the logs after using it a little, that should give you an idea of how long the requests take. The lines that start with `#` refer to Swift->Racket RPCs. Here's an example from my logs:
2025-01-28 13:04:32 +0000 [io.defn.NoiseBackend.Backend] [debug] #006381: waitForAllDownloads()
2025-01-28 13:04:32 +0000 [io.defn.NoiseBackend.Backend] [debug] #006381: took 319µs to fulfill
2025-01-28 13:04:32 +0000 [io.defn.Podcatcher.AppDelegate] [debug] didBecomeActive: finished downloading pending episodes
2025-01-28 13:04:33 +0000 [io.defn.NoiseBackend.Backend] [debug] #006382: getStats()
2025-01-28 13:04:33 +0000 [io.defn.NoiseBackend.Backend] [debug] #006382: took 2ms to fulfill
Some things on the Racket side are long running, like the download manager. It's like an actor that keeps track of what's being downloaded and the progress of each download. Whenever a download makes progress, it notifies the Swift side by making a callback from Racket->Swift. In this example, there is some duplication since both the Swift and Racket sides each have a view of the same data, but it's negligible.What's not as great from a memory use perspective is how large Racket's baseline memory use is. Loading the Racket runtime and all the app code takes up about 180MB of RAM, but then anything the app does is marginal on top of that (unless there's a bug, of course).
[1]: I did this precisely because, as you say, keeping keeping data in sync in memory between the two languages would be very hard, especially since the Racket GC is allowed to move values in memory. A value you grab at t0 might no longer be available at t1 if the Racket VM was given a chance to run between t0 and t1, so it's better to just let Racket run in its own thread and communicate with it via pipes. Probably, the same would be true for OCaml.
In my PoC (https://github.com/jbhoot/poc-ocaml-logic-native-ui) - a tiny hello world CLI on macOS, that has a Swift "frontend" and OCaml "backend" - I followed a similar model:
- Both sides pass messages to each other. Both of them talk through C ABI. My model was synchronous though. Async is certainly better. - I used the protobuf binary protocol for message passing. Faster and probably more efficient than, say, JSON. But both sides may have copies of the same data for this reason.
I've written down my approach, which roughly aligns with yours, in the project's README.
What I wanted to do was for OCaml side to allocate data in memory, and for Swift to access the same memory through some commonly agreed upon protocol (protobuf itself maybe?). But I haven't yet explored how difficult this could be with GC coming in play. I think OCaml does have a few tricks to tell GC to not collect objects being shared through C ABI (https://ocaml.org/manual/5.3/intfc.html#s:c-gc-harmony), but I haven't looked into this enough to be sure.
Your projects will sure help me figure out the concepts!
1. You mentioned it already but bookmarks. In particular bookmarks that can be added through iOS Shortcuts (so we can each build whatever crazy automation we need) and that can be exported (so we can keep LARPing productivity in our Obsidian vaults).
2. An "undo" function, somehow. I know this is weird but I've misclicked the seek bar so many times (esp. in the lock screen) and lost where I was... a solution for that would be so cool.
I've been using the same podcasts app for... well over 10 years. But yours looks really cool, I may just switch.
You can also send messages to creators and attach amounts (those are called boosts), so you'd send a short message and attach 10k sats (or whatever you want), which is about 10 euros/dollars.
Some podcasts I listen to use this to read messages on the show (the highest ones). It's fun and casual but remains highly technical for now, https://getalby.com/ takes away some friction (but you're still self hosing a lightning wallet using docker compose). Easiest I think is Fountain [0, 1], which has it all built in. But I never tried that, I use Castamatic [2] with Alby.
I have to say, it seems to divide the Linux podcast world a bit with some shows embracing this, and some just calling it crypto-bs. A well, people seem to enjoy it and creators get real value, enough to have to rely less on ads even. I think this is one of the babies we'd throw out if we banned crypto currencies (which I agree are 99.999% bs).
[0] https://apps.apple.com/us/app/fountain-podcast-player/id1576...
[1] https://play.google.com/store/apps/details?id=fm.fountain.ap...
[2] https://apps.apple.com/us/app/castamatic-podcast-player/id96...
I have a personal feed that I can connect to over TailScale. But I've found that most podcast clients have a server-side component, which means their backend server must be able to access the feed.
I tried adding my private feed to Podcatcher and am getting "Server Error (500)".
Couldn’t find a good app for phosh and was thinking of writing one.
- Privacy
- OPML support
- Ability to import from Apple Podcast
- Dark theme
I see Windbreaker Podcast featured in those screenshots on the App Store listing. A Z̶e̶r̶o̶ ̶P̶u̶n̶c̶t̶u̶a̶t̶i̶o̶n̶ Fully Ramblomatic fan?
[1] Error string::1: bytes->jsexpr: bad input starting #"error code: 502" context..: .../syntax/readerr.rkt:15:2: -raise- read-error ../private/arrow-val-first.rkt:486:18 .../private/backend.rkt:45:9: get-trending-podcasts .../noise-serde-lib/backend.rkt:69:22
Some podcasts are news related and require hearing the latest episode. Other podcasts are more serial like history narrations and require hearing the series in the order it appears. Download options should be reflective of these two functions. And "play next" should also be reflective of these two functions.
Some podcasts have fast speakers and need to be slowed down. Other podcasts have slow speakers and need to be sped up. The speed setting should be saved per podcast.
New View: A play next queue list should be present. Swipe right to play. Swipe left for other options
Condensed View: Upon expanding an episode, swipe left to see other episodes in the list ordered by date based on preference. Scroll down to see options for the episode. Scroll up to see other episodes in Queue.