Gluing Things Together
Adding missing features with a bit of Nim · March 10th, 2023
Got a quick one this time round, just wanted to do a quick once over on a little Nim program I wrote recently to better glue together Shellcaster & MOCP for podcast playback. Right, so what even is the issue? Shellcaster is a really great little TUI podcast management application, and MOCP is the venerable music on console. Both of these projects are frankly amazing in their own rights, but my issue is really with a missing feature in Shellcaster, it doesn't handle playback history for the podcast episodes it plays. It however happily passes along your episodes to whatever playback program you want, vlc, mpv, mocp, it doesn't really care. Which is great!
Because MOCP also doesn't record history..
So here we are, 8 episodes into a serial scifi drama podcast, and you keep losing you spot. It doesn't help that you primarily listen to these podcasts while you work, doing mostly trivial things like switch config. As soon as its interrupted and you lose your place in your episode there's no going back. MOCP doesn't remember, Shellcaster doesn't care, and because you were the distracted human you are you have no clue what minute your at, let alone the second so you can jump to it. It's a 1st world Sisyphean problem that requires an entire shim program to solve.
Enter moctrack! An itty bitty Nim program that will happily broker the connection between shellcaster and mocp for you, all while keeping track of an itty bitty database of your playback states. It isn't perfect, but it actually tries to at least remember for you, and can get you back to where you're supposed to be within about 10s give or take!
Under the hood it looks a little bit like this:
But from a user perspective it acts the same as if you had put "mocp -l %s" into your shellcaster configuration!
That's cool, how does it work?
Glad you asked voice in my head that helps write these posts!
At its core moctrack is a simple wrapper around the mocp query cli. We just pass query strings to the running mocp server, and parse the return into seq[string] values in our procs.
proc mocpQuery*(query: string): string = let value = execCmdEx("mocp -Q " & query) return value.output.replace("\n", "")
This simple paradigm allows us to initialize the server if it isn't running, and then continually poll it for the playback information of the file we send to mocp via shellcaster. In fact, under the hood, mocpPlay(file) is a literal call to mocp -l file. (see where this is going?).
# Start MOCP if it isn't yet running, get the last state for a file. If it doesn't exist create a new hist record, if it does, resume the file at the last recorded point proc beginPlayback(file: string) = let init = mocpCheckRunning() if init == false: discard mocpInitServer() let last = getLastState(file) if last != "": let lint = parseInt(last) rflog.log(lvlInfo, "Resuming playback @ " & last & "s") discard mocpPlay(file) if lint < 3500: sleep 3500 else: sleep lint discard mocpJump(last) sleep 3000 else: rflog.log(lvlInfo, "Starting new history.") newHistory(file, "0") discard mocpPlay(file)
The information that we poll from the server is used to determined whether or not the track was previously loaded, fetch the last recorded playback time, or enter a new entry in our playback history. We fetch the one string from mocp, and pull the information out of it that we need at the time.
It's quite simple after that, if a quick select cs from history where file == file; passes the something valid back to us we issue a mocp -j to the current second recorded in the db, or we just call mocp -l as I said before. Yes this could have been a shell script, no I did not want to do that. Nim is fun.
Anyways once we've loaded the file, we just wait for a STOP state to be returned from mocp. Assuming we haven't stopped and unloaded the file, we continue to update the history database every 10 seconds.
# Requery MOCP every 10s for info about the playback state and record it if we're still playing, wait if w're paused, and stop if we stop playback proc monitorPlayback() = while true: sleep 2000 let state = getCurrentState() if state == "PLAY": rflog.log(lvlInfo, state & " " & state & " " & state) putLastState(state, state) sleep 8000 elif state == "PAUSE": rflog.log(lvlInfo, state & " " & state & " " & state) sleep 8000 elif state == "STOP": rflog.log(lvlInfo, "Playback stopped, closing moctrack.") break
And that's pretty much it! Nice and simple, select, insert, update SQL queries, little bit of logging, lots of execCmd, and we've glued together a playback history feature for our podcast tui du jour!
Sure maybe this isn't the most exciting thing to blog about, but it's the first program I've written for fun in a while. And it's a huge win for my entertainment workflow, which totally fits the vibe from my last two posts. And I think it's a really good reminder that you don't need to write your own podcast tui just because you're missing one or two features (but I probably still will, and then I'll steal moctrack's code for said project).
Little bits of incremental progress are nice, and sometimes it's all you can muster. That's okay.