Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6ca17db
commit 600ce96
Showing
8 changed files
with
207 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,43 @@ | ||
# Anki-cli | ||
|
||
A command line interface for managing Anki decks. | ||
|
||
This is a proof-of-concept version which is currently only capable of adding new | ||
cards to your decks from a simple text file format. | ||
|
||
## Installation | ||
|
||
Requires python3 and pip already installed (e.g. `sudo apt install python3 python3-pip`) | ||
|
||
1. Install [anki-connect](https://github.com/FooSoft/anki-connect) (anki addon 2055492159) and restart Anki | ||
2. Run `pip3 install -r` in this directory | ||
|
||
|
||
## Usage | ||
|
||
Example: | ||
|
||
./bin/anki-cli --card_file docs/example-card.txt | ||
|
||
run with `-h` for more details | ||
|
||
|
||
## File format | ||
|
||
The `@` character at the beginning of a line is used to mark the name of a | ||
field. Everything else on the line is used as the field name (with leading and | ||
trailing whitespace removed), everything from the next line until the start of | ||
the next field is used as the contents of the field. | ||
|
||
For example a standard "Basic" card type would be written as: | ||
|
||
``` | ||
@Front | ||
What is a foo? | ||
@Back | ||
A particularly vicious variety of dandelion. | ||
``` | ||
|
||
No other parsing or modification is done, so html, cloze deletions, LaTeX, etc. | ||
should all work as normal. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import requests | ||
import json | ||
|
||
import re | ||
|
||
|
||
def pretty_print_request(r): | ||
""" | ||
At this point it is completely built and ready | ||
to be fired; it is "prepared". | ||
However pay attention at the formatting used in | ||
this function because it is programmed to be pretty | ||
printed and may differ from the actual request. | ||
""" | ||
print('{}\n{}\n{}\n\n{}\n{}'.format( | ||
'-----------START-----------', | ||
r.request.method + ' ' + r.request.url, | ||
'\n'.join('{}: {}'.format(k, v) for k, v in r.request.headers.items()), | ||
r.request.body, | ||
'-----------END-----------', | ||
)) | ||
print('\n{}\n{}\n{}\n{}'.format( | ||
'------START-RESPONSE-----', | ||
'Status Code: ' + str(r.status_code), | ||
r.text, | ||
'-------END-RESPONSE------', | ||
)) | ||
|
||
|
||
def anki_connect(method, params={}, debug=False): | ||
r = requests.post('http://localhost:8765', data=json.dumps({'action': method, 'params': params})) | ||
|
||
if debug: | ||
pretty_print_request(r) | ||
|
||
r.raise_for_status() | ||
|
||
if r.json() is None: | ||
raise Exception('Anki connect responded with null, this is normally an error') | ||
|
||
return r.json() | ||
|
||
|
||
def parse(string): | ||
sections = re.split('^\s*@', string, flags=re.MULTILINE) | ||
nonempty_sections = (sec for sec in sections if sec.strip() != '') | ||
|
||
out = {} | ||
for sec in nonempty_sections: | ||
lines = sec.split('\n') | ||
key = lines[0].strip().lstrip('@') | ||
value = '\n'.join(lines[1:]) | ||
out[key] = value | ||
|
||
return out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
#! /usr/bin/env python3 | ||
|
||
import sys | ||
import argparse | ||
import re | ||
|
||
import ankicli.ankicli as ankicli | ||
|
||
def parse_arguments(argv): | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument('--deck', default='Default', help = 'The Anki deck to use') | ||
parser.add_argument('--model', default='Basic', help='The model (card type) to use') | ||
parser.add_argument('--card_file', default='-', help='The file to read card data from, - means use stdin') | ||
|
||
args = parser.parse_args(argv) | ||
|
||
return args | ||
|
||
|
||
def error(*args, **kwargs): | ||
print(*args, **kwargs, file=sys.stderr) | ||
sys.exit(1) | ||
|
||
|
||
def stdin_or_file(filename): | ||
if filename == '-': | ||
return sys.stdin | ||
else: | ||
return open(filename, 'r') | ||
|
||
|
||
def main(argv): | ||
""" | ||
""" | ||
|
||
# Parse cli arguments | ||
args = parse_arguments(argv) | ||
|
||
version = ankicli.anki_connect('version') | ||
if version < 2: | ||
error('anki-cli requires anki connect version 2 or greated') | ||
|
||
decks = ankicli.anki_connect('deckNames') | ||
if args.deck not in decks: | ||
error('Bad deck name, expected one of', decks) | ||
|
||
models = ankicli.anki_connect('modelNames') | ||
if args.model not in models: | ||
error('Bad model name, expected one of', models) | ||
|
||
fields = ankicli.anki_connect('modelFieldNames', {'modelName': args.model}) | ||
|
||
with stdin_or_file(args.card_file) as card_file: | ||
card = ankicli.parse(card_file.read()) | ||
|
||
# For some reason I can't get addNote working, but this works fine. | ||
response = ankicli.anki_connect('addNotes', { | ||
'notes': [{ | ||
'deckName': args.deck, | ||
'modelName': args.model, | ||
'fields': card, | ||
'tags': [], | ||
}] | ||
}) | ||
new_card_id = response[0] | ||
|
||
if new_card_id is None: | ||
error('Failed to create card, does it have unique fields?') | ||
|
||
print('Created new card with id', response[0]) | ||
|
||
# Return success | ||
return 0 | ||
|
||
# If this script is run from a shell then run main() and return the result. | ||
if __name__ == "__main__": | ||
sys.exit(main(sys.argv[1:])) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
@Front | ||
What is a <b>foo</b>? | ||
@Back | ||
A fluffy fluffy | ||
|
||
<ul> | ||
<li>doge</li> | ||
<li>cat</li> | ||
<li>or fish</li> | ||
</ul> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
@Front | ||
What is a foo? | ||
@Back | ||
A particularly vicious variety of dandelion. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
requests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,21 @@ | ||
from nose.tools import * | ||
import ankicli | ||
import ankicli.ankicli as ankicli | ||
|
||
def test_basic(): | ||
print("I RAN!") | ||
|
||
def test_parse_single_field(): | ||
assert ankicli.parse("") == {} | ||
assert ankicli.parse("@First") == {'First': ''} | ||
assert ankicli.parse("@First\nFoo") == {'First': 'Foo'} | ||
assert ankicli.parse("@First\nFoo\n") == {'First': 'Foo\n'} | ||
assert ankicli.parse("@First\nFoo@@\n") == {'First': 'Foo@@\n'} | ||
assert ankicli.parse("@First\nFoo\nBar\nBaz") == {'First': 'Foo\nBar\nBaz'} | ||
|
||
|
||
def test_parse_whitespace(): | ||
assert ankicli.parse("@First \nFoo\n") == {'First': 'Foo\n'} | ||
assert ankicli.parse("@ First \nFoo\n") == {'First': 'Foo\n'} | ||
assert ankicli.parse(" @First") == {'First': ''} | ||
|
||
|
||
def test_parse_multi_field(): | ||
assert ankicli.parse("@First\nFoo\n@Second\nbar\n") == {'First': 'Foo\n', 'Second': 'bar\n'} |