anki integration

I use beeminder to track my anki reviews. for a long time I manually inputted a +1 when I finished, but I’d always wanted an autodata solution because I find it tedious to keep up with manual data entry for more than a couple goals at a time.

however, none of the existing solutions quite did it for me - the thing I most care about is whether I did all of my reviews, not exactly how many reviews I did (which fluctuates by day) or some other incidental statistic.

I’ve come up with an autodata extension which basically works for me. it’s not at all plug and play, but for anyone else looking to work up an autodata solution for this or some other goal, it might be helpful as simple guidelines.

# ~/.local/share/Anki2/addons21/ankibee/__init__.py

from string import Template
import requests
import os
# from anki.collection import Collection
from aqt import mw
from aqt import gui_hooks

try:
    from dotenv import load_dotenv
    load_dotenv() # loads from .env by default
except ImportError:
    pass

bee_user = os.getenv('BEEMINDER_USER')
bee_auth_token = os.getenv('BEEMINDER_AUTH_TOKEN')


bee_goal = "anki"

# is this a good way of doing this? probably not
# but it sends 0 when I haven't done any reviews, 1 when I've done all of them,
# and something in between when I've done something in between. "good enough"
def get_review_ratio():
    try:
        reviews_due = mw.col.sched.counts()[2]
    except AttributeError:
        return # something isn't set if we haven't done any reviews this session, dirty way out
    review_cards_rated = len(mw.col.find_cards("rated:1 is:review"))
    review_ratio = review_cards_rated / (reviews_due + review_cards_rated)

    return review_ratio


from aqt.utils import showInfo


def send_datapoint(goal, value, comment = 'via ankibee'):
    return requests.post(f'https://www.beeminder.com/api/v1/users/{bee_user}/goals/{goal}/datapoints.json', data = {
        'auth_token': bee_auth_token,
        'value': value,
        'requestid': mw.col.sched.today, # if we submit multiple datapoints on the same day it replaces the earlier datapoint
        'comment': comment
        })


def beemind_review_ratio():
    send_datapoint('anki', get_review_ratio())


def beemind_anki_journal():
    journal_note_count = len(mw.col.find_notes("tag:anki-journal"))

    send_datapoint('anki-journal', journal_note_count)


# TODO we also do things that are *not* beeminding # we actually don't yet ignore this
# repeatedly sending datapoints should be idempotent otherwise 	
# TODO these requests are synchronous, if we make a lot that could get annoying
def beemind_everything():
    beemind_review_ratio()
    beemind_anki_journal()


gui_hooks.profile_will_close.append(beemind_everything)
7 Likes

Thanks for sharing!

If this sends a datapoint every time the desktop anki app syncs, then you’ll also want to change the goal’s aggregation method to be max or nonzero, which you can do on a ‘custom’ goal toward the bottom of the settings tab.

2 Likes

@philip sending a requestid causes beeminder to replace the existing datapoint with that requestid. I set requestid to mw.col.sched.today, so I just get the more recent version of a datapoint recorded in anki (see the comment in send_datapoint).

you could set a goal to work differently, but since I submit datapoints every time anki closes (triggers reliably for me and only a little spammy) I like this approach, rather than than filling beeminder with junk datapoints.

if I add more specific goals (which I intend to in the future) I’ll probably want start making the requests asynchronously to avoid slowing down shutdown too much. it’s also possible to have datapoints send on other triggers but most of those would probably feel bad without async sending.

3 Likes

Anki was driving me nuts - it’s hard to replace Anki and their platform is limited for a reason. To keep it free, they had to make some constraints. Nonetheless I am not able to have plugins for Anki mobile, I can’t spin up my custom sync server and I can’t have any API endpoint to retrieve the data. I can write a bot to login to AnkiWeb and read data, but it isn’t nice solution.

I came up with a bit different one to make sure to sync data with beeminder. It should work on every platform and is quite elastic. You can execute JS in a card template. In your card template, you can add a snippet of code in either “back” or “front”:

<script>
(() => {
  if (typeof window._skcount === "undefined") {
    window._skcount = 0;
  }
  window._skcount += 1;

  const $debug = document.querySelector("#skcount");
  $debug.innerHTML = window._skcount;

  const SYNC_EVERY_X_CARDS = 10;
  const USER = "YOUR_NAME";
  const AUTH_TOKEN = "YOUR_TOKEN";
  const GOAL = "YOUR_GOAL";

  if (window._skcount % SYNC_EVERY_X_CARDS === 0) {
    fetch(`https://www.beeminder.com/api/v1/users/${USER}/goals/${GOAL}/datapoints.json?auth_token=${AUTH_TOKEN}`, {
      headers: {
        "Content-Type": "application/json",
      },
      method: "POST",
      body: JSON.stringify({ value: SYNC_EVERY_X_CARDS, comment: new Date().toString() }),
    })
      .then(() => {
        $debug.innerHTML = "Saved to Beeminder!";
      })
      .catch((error) => ($debug.innerHTML = error.message));
  }
})();
</script>

Please fill the template with your goal data and also place in your card this snippet: <div id="skcount"></div> to see which card is it and see syncing status.

This script will add a beeminder data every X cards you review. It works for me on Anki MacOS, AnkiMobile (iOS) and AnkiWeb. Obviously it has a downside if you spread your efforts thin across different decks (20 decks, 5 cards to review in each).
Similarly, we can use JavaScript tricks to beemind beenary goals (did I review at least 1 card today?) or perhaps create “time spent on Anki” goal.

9 Likes

@skorytnicki that’s brilliant!

2 Likes

Stealing this thread for one more post.

You might find interesting the auto generation of the images attached to an Anki card. I learn foreign languages with Anki, it’s much easier to remember them with some image.

My deck was originally English/Spanish, I translate it to Polish/Spanish as I go. I keep English field however to download an image. Some examples:

My Back template is this:

<div class='content'>
<div class='title'>
<!--- If "Polski" field exists, use it. If not, go with English default --->
{{^Polski}}
    {{Front}}
{{/Polski}}
{{#Polski}}
    {{Polski}}
{{/Polski}}
</div>
<div class='title translation animation'>
{{Back}}
</div>
<div class='desc animation'>
{{Description}}
</div>
<div class='image animation'>
</div>
</div>
<script>
(() => {
const url = "https://api.unsplash.com/search/photos?page=1&orientation=landscape&query="
const front = "{{Front}}"; // Use English field
const accessKey = "ADD YOUR UNSPLASH KEY"; // go to unsplash and get API key.
fetch(url + front, {
headers: {Authorization: `Client-ID ${accessKey}` }
}).then(res => res.json()).then(data => {
// can also insert img tag if you wish
 document.querySelector('.image').style.backgroundImage = `url(${data.results[0].urls.small})`;
});
})();
</script>
4 Likes

I would really appreciate if you could upload this on google drive, I have no idea why its not working for me

2 Likes

In case you ask about my solution. Here’s a sample deck. You have to go to review on your desktop and edit one card to use your beeminder credentials, like that:

Every five cards you’ll get a datapoint in your beeminder goal. In case of errors, you’ll see beeminder message:

Here you can find the token: beeminder

Good luck. Let me know if it works, in case it doesn’t, please drop the error message.

2 Likes