Using `mkvtoolnix` and `GNU Parallel` to Whip Some .ASS

I recently needed to work some mkvmerge=/=mkvpropedit magic for some anime. It’s been a long time since the days when I started watching Bleach with my friends by sshing to one laptop from another and using mplayer in a terminal to play the show and the state of mkv support has come a very long way since then.

These days I prefer playing all my media through Plex which is just hard to describe in all its towering greatness. The show that I wanted to watch though had its ASS subtitles and accompanying fonts broken out from it’s MKV files for reasons unknown to me. Because of this and the amazing malleability of MKV I decided to roll up my sleeves and fix the problem myself.

shopt -s extglob

parallel -q --tagstring {/.} --line-buffer mkvmerge \
         -o {.}_merged.mkv {} --language 0:eng {.}.en.ass \
         --attachment-description '' \
         --attachment-mime-type application/x-truetype-font \
         --attach-file {//}/'fonts/A-OTF-FutoMinA101Pro-Bold.otf' \
         … 147 similar lines …
         ::: Season*/!(*_merged).mkv

The main reasons I’m writing this up at all are several fold:

  1. Often times what I’m doing with GNU Parallel is complex enough that it warrants an actual shell function or script or it’s so simple that it more or less amounts to slapping one of the arguments onto the end. Because of this I’ve rarely explored parallel's native expansion support which this task gave me an opportunity to do.
  2. The task was complex enough also that I learned about parallel'sdouble expansion which I’m sure has been the source of many frustrating missteps in the past now that I know it’s there. Essentially bash will process the arguments to parallel first obeying all the normal rules and then pass them to the exec of parallel, then parallel will pass them again to a subshell which will parse them again through all the normal rules. Keeping this all straight in your head is not easy but I think the essential rule is probably something like “If you actually notice the double expansion you should write a function/script for the behavior and otherwise you should probably be running with -q” or something similar but better worded than that.
  3. It’s fun to mess with mkv files. ¯\_(ツ)_/¯

To review the script:

  1. We’re setting extglob because we want to be able to process only mkv files that haven’t been _merged yet.
  2. We’re invoking parallel with -q because we don’t want the subshell to word split our carefully constructed arguments again.
  3. We’re using --tagstring {/.} --line-buffer because we want to see our job output live so we know it’s working (it takes a bit to make these changes to the matroska files). It’s not directly documented AFAICT that --tagstring supports GNU Parallel expansion but I took a chance on it and was pleasantly surprised. This particular expansion is the basename-sans-extension version which seemed like a sensical tag for the logged lines.

    This feature alone is worth using parallel for when you’re really doing complex parallel work. It’s a really efficient way to provide active log lines that are still comprehensible later on. --line-buffer just makes sure that you always get a full line from a job rather than the lines from the various jobs mixing mid-line.

  4. -o {.}_merged.mkv is a nice way to express the bash-ism of ${x%.*}_merged.mkv.
  5. --language 0:eng {.}.en.ass is a bit speculative as it’s what I think I should have run but I didn’t initially since I was working off an example that didn’t include it. Nevertheless I think I’m interpreting the docs correctly. Originally I just didn’t include the --language 0:eng bit so the subtitles were attached as an unknown language and I had to then go back through and correct that with an mkvpropedit run.
  6. Then I used a bit of Emacs Keyboard Macro magic to transform the Dired fonts buffer into a series of attachment arguments so that the ASS subtitle track could be properly rendered. The parallel expansion there of {//} was also something I hadn’t seen before and is how I managed to get away with this from the root of the seasons directory rather than needing to process the seasons one at a time. That one specifically expands to the dirname of the input line or as a bash-ism ${d%/*}.
  7. Finally we’re taking arguments from the extglob that matches all the Seasons mkv files excluding the _merged files which you can see are what the parallel tasks are actually producing.

With all this I was able to completely saturate my wired connection to my Synology and efficiently process the entire show. I love the smell of burning silicon in the morning.

I hope this little foray into parallel/mkvtoolnix land teaches you something like it taught me.

Happy scripting!

`org-todo-current-tab` Has Learned Direct Support for Reader View

I’ve come to really enjoy using Reader View. It’s filled that Arc90 Readability shaped hole in my heart.

In fact, my only problem with Reader View is that it broke my org-todo-current-tab Applescript which I use to extract todo items from Chrome.

No more!

tell application "Finder" to set current_tab_handlers to (load script file "current_tab_handlers.scpt" of folder "Dropbox" of home as alias)

tell application "Google Chrome"
    set theTab to active tab of front window
    if theTab's URL contains "chrome-extension://" then
        set readerHref to execute theTab javascript "document.querySelector('iframe').contentDocument.getElementById('reader-domain').href"
        set theUrl to readerHref
        set theTitle to execute theTab javascript "document.querySelector('iframe').contentDocument.getElementById('reader-title').innerText.replaceAll(/[[\\]]/g, ' ')"
        set theAuthor to execute theTab javascript "document.querySelector('iframe').contentDocument.getElementById('reader-credits').innerText"
        set theEstimatedTime to execute theTab javascript "document.querySelector('iframe').contentDocument.getElementById('reader-estimated-time').innerText"
        set theContent to execute theTab javascript "
document.querySelector('iframe').contentDocument.getElementById('readability-page-1').innerText.split('\\n').map(
  x => x.replace(/(.{70,}?)\\s/g, '$1\\n').split('\\n').map(
    y => '   ' + y
  ).join('\\n')
).join('\\n')
                "
        set the clipboard to "** TODO " & ¬
            "[[" & theUrl & "][" & theTitle & " by " & theAuthor & "]] " & theEstimatedTime & "

   " & ¬
            (do shell script "date '+[%F %a %H:%M]'") & "

   #+begin_quote
" & ¬
            theContent & ¬
            "
   #+end_quote
"

    else
        tell current_tab_handlers to set theLink to org_current_tab()
        set the clipboard to "** TODO " & ¬
            theLink & "

   " & ¬
            (do shell script "date '+[%F %a %H:%M]'") & "
"
    end if
end tell

The best part about this is because Reader View is taking care of extracting the article for me it makes it dead simple to exract the actual text of the article and put it right in the todo. I don’t generally then read the article that way but at least I have the actual text stored for later if I want to search for it.

This script makes heavy use of Applescript’s ability to run javascript in the tab. In fact it’s arguable that the whole script should just move directly into there since that’s a more capable programming environment.

How to Check for an Entry in the `known_hosts` File

As part of my work I write provisioning scripts for ephemeral computing environments (you don’t develop anything directly on your laptop, do you!?). While I was a happy Chef user for years the unfortunate rise of Dockerfiles and the extreme and inexplicable aversion of your average developer to Chef has lead me more and more down the path of coding things in bash.

The hardest thing about a provisioning script, of course, is writing idempotent behavior. If you’re not truly in container territory it yields faster iteration time to make each step cheap in the face of having already been done.

I recently wanted to do just that for adding github.com to my ~/.ssh/known_hosts file. Initially I reached for grep only to remember two things:

  1. ~/.ssh/known_hosts doesn’t necessarily list the host in plaintext so grepping is strictly a non-starter
  2. openssh is one of the most featureful CLI projects around

It was this second realization that sent me off to the man page to discover the -F option. Specifically, if you want to know whether a known_hosts entry exists for a host, you run ssh-keygen -F <host>[:port] which exits with the expected exit statuses.

So in the end I threw together this little snippet:

if ! ssh-keygen -F github.com
then
  ssh-keyscan github.com |
    tee -a ~/.ssh/known_hosts &&
    ssh-keygen -F github.com ||
      {
        echo 'Failed to add github.com to known_hosts' >&2
        exit 1
      }
fi

Cheers!

`org-todo-current-git-branch` Makes an Org todo Heading Out of the Current Git Branch

I manage all my todos through org-mode. It’s fantastic. I find myself wanting to capture todo items quickly from various contexts using specialized logic for each one. I maintain some of these as applescripts so that I can run them outside of the context of emacs and then paste them in but sometimes the context is in fact emacs itself.

That’s the case for capturing the current git branch as an org todo.

(defun timvisher-org-todo-current-git-branch
    ()
  (interactive)
  (let ((todo-item (format "** TODO =%s%s=

   %s
"
                           (file-relative-name (magit-toplevel) "~")
                           (magit-get-current-branch)
                           (format-time-string "[%F %a %H:%M]"))))
    (kill-new todo-item)
    (message "Saved ‘%s’ to the kill ring"
             todo-item)))

The primary things this is getting me are:

  1. A nested ** TODO … entry. All my todos start out under a top-level * Inbox heading. That’s actually one of the reasons I wanted this function at all. Despite how great whacking C-u C-c C-x M is it keeps the contextual heading level which meant that I had to then whack M-→ to get it properly indented. With this it starts that way.

  2. It wouldn’t be hard to actually link to the branch but since I do all of my work in dedicated tmux sessions this doesn’t really make sense. Oooo now that I think about it wouldn’t it be nice to have support for linking to a tmux session…

    /me makes a note.

  3. I capture the time that the todo was created accurately.

Cheers!

Emacs Lisp: A Small Wrapper for Gruber’s `titlecase`

I write pretty much everything I write that’s more than 280 characters in Emacs. A lot of that has a title associated with it and ever since discovering it my preferred way to title case text is to run it through Gruber’s titlecase script.

Emacs already has fantastic facilities for running shell commands, one of which is C-u M-| … which will run a command on the region (apparently even if it’s non-contiguous) and replace it with the output. This is how I’ve been doing it for years but there’s just one problem: titlecase adds a trailing newline to the text which I then need to clean up.

I ran across titlecase.el around the same time I found titlecase itself but for whatever reason I decided it wasn’t worth my time to install. I finally pilfered the parts of it I wanted recently and it’s been the dream I always though it would be.

(defun timvisher-titlecase
    (begin end)
  (interactive "*r")
  (unless (region-active-p)
    (error "Must be called with an active region"))
  (let* ((pt (point))
         (source-text (delete-and-extract-region begin end))
         (titlecased-text (with-temp-buffer
                            (insert source-text)
                            (call-process-region (point-min) (point-max) "titlecase" t t nil)
                            ;; skip trailing newline
                            (buffer-substring-no-properties (point-min) (1- (point-max))))))
    (insert titlecased-text)
    (goto-char pt)))

Applescripts for Creating and Closing Zoom Meetings

I think it’s widely understood that we’re all using Zoom a lot more since March of 2020. I was inspired by something I didn’t write down to write up an Applescript for creating a New Zoom Meeting so I can get more ‘Fastest Gun’ awards from my co-workers. While I was at it I also wrote a Close Zoom Meeting script that automates getting me back to blissful private office work mode.

Here they are:

New Zoom Meeting.applescript:

tell application "zoom.us" to activate

tell application "System Events"

    tell process "zoom.us"

        repeat until window "Zoom Meeting" exists
            keystroke "v" using {command down, control down}
            delay 1
        end repeat

    end tell

    if (name of application processes whose background only is false) contains "VLC" then
        tell application "VLC"
            set audio volume to 64
        end tell
    end if

end tell

Close Zoom Meeting.applescript

-- Set my media player and system volumes back to my typical listening level
set volume output volume 69
tell application "System Events"
    if (name of application processes whose background only is false) contains "VLC" then
        tell application "VLC"
            set audio volume to 160
        end tell
    end if
end tell

-- Tell zoom to quit but this will likely fail because there's an active meeting and it will dialog with me about whether to really leave the meeting.
try
    tell application "zoom.us" to quit
end try

-- So long as zoom.us is still active just whack enter until it goes away.
tell application "System Events"
    repeat while name of processes contains "zoom.us"
        -- The double check here is necessary because otherwise you occasionally send an enter to the next active application
        if name of processes contains "zoom.us" then
            tell process "zoom.us"
                key code 36
            end tell
        end if
        delay 1
    end repeat
end tell

-- Close all the zoom.us tabs in my browser
tell application "Google Chrome"
    repeat with tabList in (tabs of windows whose URL contains "zoom.us")
        close tabList
    end repeat
end tell

Obviously you may not use VLC or Chrome but these script should be pretty easy to adapt to whatever you’re workflow actually is.

Isn’t Applescript fun?

Effective Applescript: Opening a Google Chrome Bookmark Folder

I’ve been attempting to live my life 30 minutes at a time recently and part of that is ensuring that I’m not constantly distracted by my inbox. Unfettered access to attention is the devil’s own work and as little as possible should be allowed.

As such I’ve been trying to make it a practice recently of shutting down every app and site that acts as an inbox for me unless I’m actively engaged with it. My practice then is to essentially check my inbox every 30 minutes or so in case something truly urgent has come in and practice Inbox Zero each time, moving anything that takes more than 2 minutes but is not truly urgent into my todo list.

Doing this every 30 minutes was starting to become annoying despite how effortless it is to open apps and sites using Quicksilver so I decide to take it a step further and automate it with Applescript so I can simply whack ⌘-space inbox RET and get to down to business.

tell application "Google Chrome"
    repeat with b in (get URL of bookmark items of bookmark folder "Inbox" of bookmark folder "Bookmarks Bar")
        open location b
    end repeat
end tell

tell application "Mail" to activate
tell application "Slack" to activate

The primary reason I’m posting about this generally uninteresting script is because at first I actually had all the bookmarks in my “Inbox” folder explicitly listed out in separate open location … statements. This was obviously less than ideal because I decided to add Twitter to my Inbox and would’ve then had to go and add it to the script by hand.

I did a little bit of digging and found out that the Chrome dictionary does, in fact, support accessing bookmarks by folder. Then a quick trip to Stack Overflow got me over the hump.

As an aside I originally had the statement repeat with b in (URL of bookmark items… rather than repeat with b in (get URL of bookmark items… but was erroring out with an error code that didn’t yield anything obvious (but what Applescript error codes ever do?). It’s unclear to me why the get is necessary here. get is one of those things that I still sometimes just start sprinkling over my Applescripts until they seem to work which is never a good place to be.

Happy scripting!

Applescript to Jiggle a Window

I have this ridiculous problem (because I am a developer in the year of our Lord 2021) where occasionally as I disconnect and reconnect my monitors on my laptop, iTerm windows will be resized by the window manager but the $COLUMNS and $LINES variables won’t get updated. I have no idea what actually causes this but it really plays havoc with my prompts/emacs/tmux setup.

After some experimentation I realized that I could force them to be updated by jiggling the size of the window just a small amount manually. But, as I said, I am a developer in the year of our Lord 2021; reaching for the mouse and aiming at the edge of the window was just too hard.

Enter my beloved Applescript:

tell application "iTerm"
    set currentBounds to bounds of front window

    set width to item 3 of currentBounds

    copy currentBounds to tempBounds

    set item 3 of tempBounds to (width - (width * 0.1))

    set bounds of front window to tempBounds

    delay 0.25

    set bounds of front window to currentBounds
end tell

Now instead of reaching for my mouse I can whack ⌘-space jiggle window RET and we’re back in business.

Now this is productivity!

/ht alvinalexander.com and techbarrack.com

Eternal Terminal Is an Alternative to Mosh With Support for Tmux -CC and Native Scrolling Over TCP

Eternal Terminal just rolled through my Inbox. While it doesn’t sound like it has anything that I’m particularly interested in over mosh a couple things caught my eye:

  1. It uses TCP rather than UDP which could be attractive for some firewall scenarios. Specifically it almost seems like a proof of concept for the underlying ‘resumable TCP’ implementation that apparently any application can use.
  2. It supports tmux -CC. For people who make heavy use of tmux features that want a 'native’ experience in iTerm2 this sounds pretty great.
  3. It doesn’t have scrolling problems like mosh does since, IIUC, it actually does attach your terminal directly to the remote session. You get native scroll bars, native search, etc. because of that.

Maybe this’ll scratch an itch for you that I don’t have?

Bite Size Bash by b0rk is awesome!

@b0rk is at it again with another fanastic zine. This one’s about a topic that’s unusually near and dear to me so I absolutely couldn’t resist snapping it up.

Bite Size Bash is everything you should expect from a Julia Evans zine: comprehensive, insightful, and fun. While I have a few quibbles with the content (I’m sorry but set -e is the devil’s own work, and please don’t read a file or anything else line by line with a for loop) but these little quibbles are far outweighed by the mountain of other good and cogent content in the zine.

I especially love her descriptions of what bash is particularly good at: Gluing processes together, concurrency management, and execution tracing, for a small sample.

If you’ve been ignoring my advice about diving into Greg’s Bash Guide (or even if you haven’t been) I would whole-heartedly recommend grabbing yourself a copy of Bite Size Bash and getting familiar with one of the most pragmatic tools available to anyone developing software (or operating in any other way) a *nix system.