Anki: Insert the most recent image

I make a lot of Anki cards, so I’m on a constant quest to make the process more efficient. Like a lot of language-learners, I use images on my cards where possible in order to make the word or sentence more memorable.

Process

When I find an image online that I want to use on the card, I download it to ~/Documents/ankibound. A Hazel rule then grabs the image file and converts it to a .webp file with relatively low quality and a maximum horizontal dimension of 200px. The size and low quality allow me to store lots of images without overwhelming storage capacity, or more importantly, resulting in long synchronization times.

But, getting the “done” image into Anki is a hole in the process. Yes, I could click the paperclip “insert media” button and browse to the file. Or I could open a Finder window and drag the file into the image field. But a faster solution is to use AnkiConnect to store the image file and then just insert an <img> tag into the field HTML, then close up. If we set this up as a Keyboard Maestro macro, it’s just a quick keystroke combination.

Here’s how it works:

Finding the most recent file

We’re going to use bash for this because the composability of its tools make this kind of filesystem work so easy. Here’s the first part of our script.

#!/bin/bash

DIR="$HOME/Documents/ankidone"
fn=$(ls -lt $DIR | head -2 | tail -1 | awk '{print $9}')

lt -lt gives us a list of files in $DIR ordered by modification date. Then head -2 returns the first two lines. Why 2 lines? Well ls insists on printing out a total _n_ line first, so we need to grab that line and the one we want which is next. We find our most recent file line with a call to tail -1. Finally awk splits the line into space-separate fields. The filename is the ninth field. So $fn now has our filename and extension.

Storing the file in collection.media

We will use AnkiConnect to store the file for us.

ip="http://localhost:8765/"
action="storeMediaFile"
json=$(jo -p version=6 action=$action params[filename]=$fn params[path]="$DIR/$fn")
r=$(curl -s -XPOST "$ip" \
   -H 'Content-Type: application/json' \
   -d "$json"
)

The hard-coded $ip address is just the standard address of the AnkiConnect server running inside of Anki. The API action we’re interested in is storeMediaFile, for obvious reasons. Next we use the excellent jo tool1 to build the required JSON payload for the POST API call.

Inserting the image into the editor

In our Keyboard Maestro macro, we set the Execute Shell Script action to same the result on the clipboard. The last part of our shell script formats an <img> HTML tag for us to insert. That’s what’s on the clipboard when the shell script exits.

The macro assumes the cursor is sitting in the image field. So with a command+shift+X we can open the HTML field editor. Then just a paste keystroke, and repeat the command+shift+X and the image should appear. Done!

There are definitely other ways we could go about this, that don’t involve AnkiConnect. It’s possible that just moving the image into collection.media would suffice. But then we would have to hard-code the proper path to that directory and if you use multiple collections, you’d be out of luck.

For reference, here’s the whole script:

#!/bin/bash

DIR="$HOME/Documents/ankidone"
fn=$(ls -lt $DIR | head -2 | tail -1 | awk '{print $9}')

ip="http://localhost:8765/"
action="storeMediaFile"
json=$(jo -p version=6 action=$action params[filename]=$fn params[path]="$DIR/$fn")
r=$(curl -s -XPOST "$ip" \
   -H 'Content-Type: application/json' \
   -d "$json"
)
echo "<img src=\"$fn\">"

Happy studying!


  1. jo is non-standard on my system. On macOS you can install via brew install jo. If you work frequently with JSON on the command line, it saves a lot of time. More about the jo here. ↩︎

Altering Anki's revlog table, or how to recover your streak

Anki users are protective of their streak - the number of consecutive days they’ve done their reviews. Right now, for example, my streak is 621 days. So if you miss a day for whatever reason, not only do you have to deal with double the number of reviews, but you also deal with the emotional toll of having lost your streak.

You can lose your streak for one of several reasons. You could have simply been lazy. You may have forgotten that you didn’t do your Anki. Or travel across timezones put you in a situation where Anki’s clock and your clock differ. Others have described a procedure for resetting the computer’s clock as a way of recovering a lost streak. It apparently works though I haven’t tried it. Instead I’ll focus on a technique that involves working directly with the Anki database.

First, I must warn you: if you don’t know anything about sqlite, SQL or the like don’t attempt this. It’s very easy to do something that will wreck your collection’s database. You’ve been warned.

Locating the database

First of all, make any modifications to the Anki db with care; make backups, etc. Make sure Anki is closed.

On macOS the sqlite database file is at ~/Library/Application Support/Anki2/your_collection_name/collection.anki2. On other platforms, the path is something else, I’m sure the manual says something about this. Or you can just search for collection.anki2 and navigate there. That file is the Anki database.

Moving a review

Let’s say the goal is to move the latest review back to a different day. This query will find the latest review (here I’ve restricted to a particular deck named Словарный запас…)

SELECT * FROM revlog r 
INNER JOIN cards c ON c.id = r.cid 
INNER JOIN decks d ON c.did = d.id
WHERE d.name LIKE '%Словарный запас%'
   AND c.queue = 2
ORDER BY r.id DESC
LIMIT 1

This query returns a single row. In that row, focusing on the id column, that will give us the timestamp in Unix epoch milliseconds. In my case it is 1656057796342. That translates to Fri Jun 24 2022 08:03:16 GMT+0000, which checks out.

Now we are going to need to move that row to an id with a different timestamp. But what should that timestamp be? Well, if we want to move it to yesterday, then we can subtract 86400 seconds (the number of seconds in one day) from the id above. Remember that the id field is in epoch milliseconds so we have to divide by 1000 first → 1656057796. Now subtract 86400 → 1655971396, and multiply by 1000 → 1655971396000 which translates to Thu Jun 23 2022 08:03:16 GMT+0000. Bingo! One day earlier that our existing row.

Now we just need to move the row to our new computed id:

UPDATE revlog 
SET id = 1655971396000
WHERE id = 1656057796342

As long as our new id is unique, this query should succeed.

Again, if you’re not comfortable working with sqlite and SQL queries - this is not a good idea. At the very minimum backup your database in case of error.

References

A deep dive into my Anki language learning: Part III (Sentences)

Welcome to Part III of a deep dive into my Anki language learning decks. In Part I I covered the principles that guide how I setup my decks and the overall deck structure. In the lengthy Part II I delved into my vocabulary deck. In this installment, Part III, we’ll cover my sentence decks. Principles First, sentences (and still larger units of language) should eventually take precedence in language study. What help is it to know the word for “tomato” in your L2, if you don’t know how to slice a tomato, how to eat a tomato, how to grow a tomato plant?

A deep dive into my Anki language learning: Part II (Vocabulary)

In Part I of my series on my Anki language-learning setup, I described the philosophy that informs my Anki setup and touched on the deck overview. Now I’ll tackle the largest and most complex deck(s), my vocabulary decks. First some FAQ’s about my vocabulary deck: Do you organize it as L1 → L2 or as L2 → L1, or both? Actually, it’s both and more. Keep reading. Do you have separate subdecks by language level, or source, or some other characteristic?

A deep dive into my Anki language learning: Part I (Overview and philosophy)

Although I’ve been writing about Anki for years, it’s been in bits and pieces. Solving little problems. Creating efficiencies. But I realized that I’ve never taken a top-down approach to my Anki language learning system. So consider the post the launch of that overdue effort. Caveats A few caveats at the outset: I’m not a professional language tutor or pedagogue of any sort really. Much of what I’ve developed, I’ve done through trial-and-error, some intuition, and a some reading on relevant topics.

A tool for scraping definitions of Russian words from Wikitionary

In my perpetual attempt to make my language learning process using Anki more efficient, I’ve written a tool to extract English-language definitions from Russian words from Wiktionary. I wrote about the idea previously in Scraping Russian word definitions from Wikitionary: utility for Anki but it relied on the WiktionaryParser module which is good but misses some important edge cases. So I rolled up my sleeves and crafted my own solution. As with WiktionaryParser the heavy-lifting is done by the Beautiful Soup parser.

Getting plaintext into Anki fields on macOS: An update

A few years ago, I wrote about my problems with HTML in Anki fields. If you check out that previous post you’ll get the backstory about my objection. The gist is this: If you copy something from the web, Anki tries to maintain the formatting. Basically it just pastes the HTML off the clipboard. Supposedly, Anki offers to strip the formatting with Shift-paste, but I’ve point out to the developer specific examples where this fails.

Thursday, May 26 2022

I would like to propose a constitutional amendment that prohibits Sen. Ted Cruz (F-TX)1 from speaking or tweeting for seven days after a national tragedy. I’d also be fine with an amendment that prohibits him from speaking ever. The “F” designation stands for Fascist. The party to which Cruz nominally belongs is more aligned with WW2-era Axis dictatorships than those of a legitimate free civil democracy. ↩︎

Extracting title title of a web page from the command line

I was using a REST API at https://textance.herokuapp.com/title but it seems awfully fragile. Sure enough this morning, the entire application is down. It’s also not open-source and I have no idea who actually runs this thing. Here’s the solution: #!/bin/bash url=$(pbpaste) curl $url -so - | pup 'meta[property=og:title] attr{content}' It does require pup. On macOS, you can install via brew install pup. There are other ways using regular expressions but no dependency on pup but parsing HTML with regex is not such a good idea.