2021-01-18 06:15:48 +00:00
# Copyright 2016-2021 Alex Yatskov
2016-05-21 22:10:12 +00:00
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2022-04-13 23:42:13 +00:00
import aqt
2024-11-07 00:18:56 +00:00
required_anki_version = ( 23 , 10 , 0 )
2022-04-13 23:42:13 +00:00
anki_version = tuple ( int ( segment ) for segment in aqt . appVersion . split ( " . " ) )
2024-04-25 08:18:00 +00:00
if anki_version < required_anki_version :
2024-10-06 15:10:16 +00:00
raise Exception ( f " Minimum Anki version supported: { required_anki_version [ 0 ] } . { required_anki_version [ 1 ] } . { required_anki_version [ 2 ] } " )
2022-04-13 23:42:13 +00:00
2017-08-22 07:53:35 +00:00
import base64
2021-04-12 04:26:03 +00:00
import glob
2016-07-17 05:01:23 +00:00
import hashlib
2017-07-04 19:35:04 +00:00
import inspect
2016-05-21 22:10:12 +00:00
import json
2017-08-28 20:15:42 +00:00
import os
2017-01-30 01:34:18 +00:00
import os . path
2022-04-20 17:30:20 +00:00
import platform
2017-08-21 14:10:57 +00:00
import re
2020-01-05 23:42:08 +00:00
import time
import unicodedata
2017-02-19 20:46:40 +00:00
2020-01-05 23:42:08 +00:00
import anki
2020-03-17 05:29:49 +00:00
import anki . exporting
import anki . storage
2021-03-05 04:06:25 +00:00
from anki . cards import Card
2021-05-07 02:11:03 +00:00
from anki . consts import MODEL_CLOZE
2020-03-17 05:29:49 +00:00
from anki . exporting import AnkiPackageExporter
2020-05-01 16:19:47 +00:00
from anki . importing import AnkiPackageImporter
2021-03-05 04:06:25 +00:00
from anki . notes import Note
2022-04-13 23:42:13 +00:00
from anki . errors import NotFoundError
2024-02-18 19:45:10 +00:00
from anki . scheduler . base import ScheduleCardsAsNew
2022-04-12 22:09:04 +00:00
from aqt . qt import Qt , QTimer , QMessageBox , QCheckBox
2017-02-19 20:46:40 +00:00
2022-04-20 22:00:24 +00:00
from . web import format_exception_reply , format_success_reply
2022-03-30 19:26:26 +00:00
from . edit import Edit
2020-01-06 01:41:34 +00:00
from . import web , util
2016-05-21 22:10:12 +00:00
2017-02-19 20:07:10 +00:00
#
2018-05-07 00:52:24 +00:00
# AnkiConnect
2016-05-21 22:10:12 +00:00
#
2018-05-06 19:59:31 +00:00
class AnkiConnect :
def __init__ ( self ) :
2018-06-30 18:23:13 +00:00
self . log = None
2022-03-30 18:43:31 +00:00
self . timer = None
self . server = web . WebServer ( self . handler )
def initLogging ( self ) :
2020-01-05 23:42:08 +00:00
logPath = util . setting ( ' apiLogPath ' )
if logPath is not None :
self . log = open ( logPath , ' w ' )
2018-05-06 19:59:31 +00:00
2022-03-30 18:43:31 +00:00
def startWebServer ( self ) :
2018-05-06 19:59:31 +00:00
try :
self . server . listen ( )
2022-03-30 18:43:31 +00:00
# only keep reference to prevent garbage collection
2018-05-06 19:59:31 +00:00
self . timer = QTimer ( )
self . timer . timeout . connect ( self . advance )
2020-01-05 23:42:08 +00:00
self . timer . start ( util . setting ( ' apiPollInterval ' ) )
2018-05-06 19:59:31 +00:00
except :
QMessageBox . critical (
self . window ( ) ,
' AnkiConnect ' ,
2020-01-05 23:42:08 +00:00
' Failed to listen on port {} . \n Make sure it is available and is not in use. ' . format ( util . setting ( ' webBindPort ' ) )
2018-05-06 19:59:31 +00:00
)
2021-12-27 06:21:21 +00:00
def save_model ( self , models , ankiModel ) :
2022-04-13 23:42:13 +00:00
models . update_dict ( ankiModel )
2018-05-06 19:59:31 +00:00
2020-01-05 23:42:08 +00:00
def logEvent ( self , name , data ) :
if self . log is not None :
self . log . write ( ' [ {} ] \n ' . format ( name ) )
json . dump ( data , self . log , indent = 4 , sort_keys = True )
self . log . write ( ' \n \n ' )
2020-01-06 00:24:20 +00:00
self . log . flush ( )
2020-01-05 23:42:08 +00:00
2018-05-06 19:59:31 +00:00
def advance ( self ) :
self . server . advance ( )
def handler ( self , request ) :
2020-01-05 23:42:08 +00:00
self . logEvent ( ' request ' , request )
2018-06-30 18:23:13 +00:00
2018-05-06 19:59:31 +00:00
name = request . get ( ' action ' , ' ' )
version = request . get ( ' version ' , 4 )
params = request . get ( ' params ' , { } )
2020-01-06 00:24:20 +00:00
key = request . get ( ' key ' )
2018-05-06 19:59:31 +00:00
try :
2021-05-08 03:33:06 +00:00
if key != util . setting ( ' apiKey ' ) and name != ' requestPermission ' :
2020-01-06 00:24:20 +00:00
raise Exception ( ' valid api key must be provided ' )
2018-05-06 19:59:31 +00:00
method = None
2020-01-06 00:24:20 +00:00
2018-05-06 19:59:31 +00:00
for methodName , methodInst in inspect . getmembers ( self , predicate = inspect . ismethod ) :
apiVersionLast = 0
apiNameLast = None
if getattr ( methodInst , ' api ' , False ) :
for apiVersion , apiName in getattr ( methodInst , ' versions ' , [ ] ) :
if apiVersionLast < apiVersion < = version :
apiVersionLast = apiVersion
apiNameLast = apiName
if apiNameLast is None and apiVersionLast == 0 :
apiNameLast = methodName
if apiNameLast is not None and apiNameLast == name :
method = methodInst
break
if method is None :
raise Exception ( ' unsupported action ' )
2019-06-01 18:06:08 +00:00
2022-04-20 22:00:24 +00:00
api_return_value = methodInst ( * * params )
reply = format_success_reply ( version , api_return_value )
2019-06-01 18:06:08 +00:00
2018-05-06 19:59:31 +00:00
except Exception as e :
2022-04-20 22:00:24 +00:00
reply = format_exception_reply ( version , e )
2018-05-06 19:59:31 +00:00
2020-01-05 23:42:08 +00:00
self . logEvent ( ' reply ' , reply )
2018-06-30 18:23:13 +00:00
return reply
2018-05-06 19:59:31 +00:00
def window ( self ) :
return aqt . mw
def reviewer ( self ) :
2018-05-07 00:52:24 +00:00
reviewer = self . window ( ) . reviewer
if reviewer is None :
raise Exception ( ' reviewer is not available ' )
2021-01-18 06:13:27 +00:00
return reviewer
2018-05-06 19:59:31 +00:00
def collection ( self ) :
2018-05-07 00:52:24 +00:00
collection = self . window ( ) . col
if collection is None :
raise Exception ( ' collection is not available ' )
2021-01-18 06:13:27 +00:00
return collection
2018-05-06 19:59:31 +00:00
2018-05-07 05:13:21 +00:00
def decks ( self ) :
decks = self . collection ( ) . decks
if decks is None :
raise Exception ( ' decks are not available ' )
2021-01-18 06:13:27 +00:00
return decks
2018-05-07 05:13:21 +00:00
2018-05-06 19:59:31 +00:00
def scheduler ( self ) :
2018-05-07 00:52:24 +00:00
scheduler = self . collection ( ) . sched
if scheduler is None :
raise Exception ( ' scheduler is not available ' )
2021-01-18 06:13:27 +00:00
return scheduler
2018-05-06 19:59:31 +00:00
2018-05-07 21:18:20 +00:00
def database ( self ) :
database = self . collection ( ) . db
if database is None :
raise Exception ( ' database is not available ' )
2021-01-18 06:13:27 +00:00
return database
2018-05-07 21:18:20 +00:00
2018-05-06 19:59:31 +00:00
def media ( self ) :
2018-05-07 00:52:24 +00:00
media = self . collection ( ) . media
if media is None :
raise Exception ( ' media is not available ' )
2021-01-18 06:13:27 +00:00
return media
2018-05-06 19:59:31 +00:00
2022-12-24 11:10:56 +00:00
def getModel ( self , modelName ) :
2024-01-21 14:31:21 +00:00
model = self . collection ( ) . models . by_name ( modelName )
2022-12-24 11:10:56 +00:00
if model is None :
raise Exception ( ' model was not found: {} ' . format ( modelName ) )
return model
2023-04-14 01:30:49 +00:00
def getField ( self , model , fieldName ) :
2024-01-21 14:31:21 +00:00
fieldMap = self . collection ( ) . models . field_map ( model )
2022-12-24 11:10:56 +00:00
if fieldName not in fieldMap :
2023-04-14 01:30:49 +00:00
raise Exception ( ' field was not found in {} : {} ' . format ( model [ ' name ' ] , fieldName ) )
2022-12-24 11:10:56 +00:00
return fieldMap [ fieldName ] [ 1 ]
2023-04-14 01:30:49 +00:00
def getTemplate ( self , model , templateName ) :
for ankiTemplate in model [ ' tmpls ' ] :
if ankiTemplate [ ' name ' ] == templateName :
return ankiTemplate
raise Exception ( ' template was not found in {} : {} ' . format ( model [ ' name ' ] , templateName ) )
2018-05-07 00:52:24 +00:00
def startEditing ( self ) :
self . window ( ) . requireReset ( )
def createNote ( self , note ) :
2018-05-06 19:59:31 +00:00
collection = self . collection ( )
2024-01-21 14:31:21 +00:00
model = collection . models . by_name ( note [ ' modelName ' ] )
2018-05-06 19:59:31 +00:00
if model is None :
2018-05-07 00:52:24 +00:00
raise Exception ( ' model was not found: {} ' . format ( note [ ' modelName ' ] ) )
2018-05-06 19:59:31 +00:00
2024-01-21 14:31:21 +00:00
deck = collection . decks . by_name ( note [ ' deckName ' ] )
2018-05-06 19:59:31 +00:00
if deck is None :
2018-05-07 00:52:24 +00:00
raise Exception ( ' deck was not found: {} ' . format ( note [ ' deckName ' ] ) )
2018-05-06 19:59:31 +00:00
2018-05-07 00:52:24 +00:00
ankiNote = anki . notes . Note ( collection , model )
2024-01-21 14:31:21 +00:00
ankiNote . note_type ( ) [ ' did ' ] = deck [ ' id ' ]
2020-05-02 21:18:31 +00:00
if ' tags ' in note :
ankiNote . tags = note [ ' tags ' ]
2018-05-06 19:59:31 +00:00
2018-05-07 00:52:24 +00:00
for name , value in note [ ' fields ' ] . items ( ) :
2021-03-14 00:10:58 +00:00
for ankiName in ankiNote . keys ( ) :
if name . lower ( ) == ankiName . lower ( ) :
ankiNote [ ankiName ] = value
break
2018-05-06 19:59:31 +00:00
2022-09-12 14:07:59 +00:00
self . addMediaFromNote ( ankiNote , note )
2018-11-07 20:05:02 +00:00
allowDuplicate = False
2020-04-23 23:39:11 +00:00
duplicateScope = None
2020-11-02 00:14:30 +00:00
duplicateScopeDeckName = None
duplicateScopeCheckChildren = False
2021-07-13 02:46:22 +00:00
duplicateScopeCheckAllModels = False
2021-01-18 06:13:27 +00:00
2018-11-07 20:05:02 +00:00
if ' options ' in note :
2021-07-13 02:46:22 +00:00
options = note [ ' options ' ]
if ' allowDuplicate ' in options :
allowDuplicate = options [ ' allowDuplicate ' ]
2021-01-18 06:13:27 +00:00
if type ( allowDuplicate ) is not bool :
raise Exception ( ' option parameter " allowDuplicate " must be boolean ' )
2021-07-13 02:46:22 +00:00
if ' duplicateScope ' in options :
duplicateScope = options [ ' duplicateScope ' ]
if ' duplicateScopeOptions ' in options :
duplicateScopeOptions = options [ ' duplicateScopeOptions ' ]
2021-01-18 06:13:27 +00:00
if ' deckName ' in duplicateScopeOptions :
duplicateScopeDeckName = duplicateScopeOptions [ ' deckName ' ]
if ' checkChildren ' in duplicateScopeOptions :
duplicateScopeCheckChildren = duplicateScopeOptions [ ' checkChildren ' ]
if type ( duplicateScopeCheckChildren ) is not bool :
raise Exception ( ' option parameter " duplicateScopeOptions.checkChildren " must be boolean ' )
2021-07-13 02:46:22 +00:00
if ' checkAllModels ' in duplicateScopeOptions :
duplicateScopeCheckAllModels = duplicateScopeOptions [ ' checkAllModels ' ]
if type ( duplicateScopeCheckAllModels ) is not bool :
raise Exception ( ' option parameter " duplicateScopeOptions.checkAllModels " must be boolean ' )
2021-01-18 06:13:27 +00:00
duplicateOrEmpty = self . isNoteDuplicateOrEmptyInScope (
ankiNote ,
deck ,
collection ,
duplicateScope ,
duplicateScopeDeckName ,
2021-07-13 02:46:22 +00:00
duplicateScopeCheckChildren ,
duplicateScopeCheckAllModels
2021-01-18 06:13:27 +00:00
)
2018-05-06 19:59:31 +00:00
if duplicateOrEmpty == 1 :
2018-05-07 00:52:24 +00:00
raise Exception ( ' cannot create note because it is empty ' )
2018-05-06 19:59:31 +00:00
elif duplicateOrEmpty == 2 :
2021-01-18 06:13:27 +00:00
if allowDuplicate :
return ankiNote
2018-06-20 16:52:46 +00:00
raise Exception ( ' cannot create note because it is a duplicate ' )
2020-12-06 05:23:03 +00:00
elif duplicateOrEmpty == 0 :
2018-05-07 00:52:24 +00:00
return ankiNote
else :
raise Exception ( ' cannot create note for unknown reason ' )
2018-05-06 19:59:31 +00:00
2021-01-18 06:13:27 +00:00
2021-07-13 02:46:22 +00:00
def isNoteDuplicateOrEmptyInScope (
self ,
note ,
deck ,
collection ,
duplicateScope ,
duplicateScopeDeckName ,
duplicateScopeCheckChildren ,
duplicateScopeCheckAllModels
) :
# Returns: 1 if first is empty, 2 if first is a duplicate, 0 otherwise.
# note.dupeOrEmpty returns if a note is a global duplicate with the specific model.
# This is used as the default check, and the rest of this function is manually
# checking if the note is a duplicate with additional options.
if duplicateScope != ' deck ' and not duplicateScopeCheckAllModels :
2021-01-18 06:13:27 +00:00
return note . dupeOrEmpty ( ) or 0
2020-02-02 21:45:05 +00:00
2021-07-13 02:46:22 +00:00
# Primary field for uniqueness
2020-05-09 00:16:05 +00:00
val = note . fields [ 0 ]
2020-12-06 05:23:03 +00:00
if not val . strip ( ) :
return 1
2024-10-05 11:17:46 +00:00
csum = anki . utils . field_checksum ( val )
2021-07-13 02:46:22 +00:00
# Create dictionary of deck ids
dids = None
if duplicateScope == ' deck ' :
did = deck [ ' id ' ]
if duplicateScopeDeckName is not None :
2024-01-21 14:31:21 +00:00
deck2 = collection . decks . by_name ( duplicateScopeDeckName )
2021-07-13 02:46:22 +00:00
if deck2 is None :
# Invalid deck, so cannot be duplicate
return 0
did = deck2 [ ' id ' ]
dids = { did : True }
if duplicateScopeCheckChildren :
for kv in collection . decks . children ( did ) :
dids [ kv [ 1 ] ] = True
# Build query
query = ' select id from notes where csum=? '
queryArgs = [ csum ]
if note . id :
query + = ' and id!=? '
queryArgs . append ( note . id )
if not duplicateScopeCheckAllModels :
query + = ' and mid=? '
queryArgs . append ( note . mid )
# Search
for noteId in note . col . db . list ( query , * queryArgs ) :
if dids is None :
# Duplicate note exists in the collection
return 2
# Validate that a card exists in one of the specified decks
for cardDeckId in note . col . db . list ( ' select did from cards where nid=? ' , noteId ) :
2020-11-02 00:14:30 +00:00
if cardDeckId in dids :
return 2
2021-01-18 06:13:27 +00:00
2021-07-13 02:46:22 +00:00
# Not a duplicate
2020-12-06 05:23:03 +00:00
return 0
2020-02-02 21:45:05 +00:00
2023-01-10 04:23:43 +00:00
def raiseNotFoundError ( self , errorMsg ) :
2022-12-31 03:26:58 +00:00
if anki_version < ( 2 , 1 , 55 ) :
raise NotFoundError ( errorMsg )
raise NotFoundError ( errorMsg , None , None , None )
2021-03-05 04:06:25 +00:00
def getCard ( self , card_id : int ) - > Card :
try :
2024-10-05 11:31:17 +00:00
return self . collection ( ) . get_card ( card_id )
2021-03-05 04:06:25 +00:00
except NotFoundError :
2023-01-10 04:23:43 +00:00
self . raiseNotFoundError ( ' Card was not found: {} ' . format ( card_id ) )
2021-03-05 04:06:25 +00:00
def getNote ( self , note_id : int ) - > Note :
try :
2024-10-05 11:31:17 +00:00
return self . collection ( ) . get_note ( note_id )
2021-03-05 04:06:25 +00:00
except NotFoundError :
2023-01-10 04:23:43 +00:00
self . raiseNotFoundError ( ' Note was not found: {} ' . format ( note_id ) )
2018-05-07 02:11:54 +00:00
2022-05-03 21:59:33 +00:00
def deckStatsToJson ( self , due_tree ) :
2022-05-23 17:37:18 +00:00
deckStats = { ' deck_id ' : due_tree . deck_id ,
' name ' : due_tree . name ,
' new_count ' : due_tree . new_count ,
' learn_count ' : due_tree . learn_count ,
' review_count ' : due_tree . review_count }
if anki_version > ( 2 , 1 , 46 ) :
# total_in_deck is not supported on lower Anki versions
deckStats [ ' total_in_deck ' ] = due_tree . total_in_deck
return deckStats
2022-05-03 21:59:33 +00:00
def collectDeckTreeChildren ( self , parent_node ) :
allNodes = { parent_node . deck_id : parent_node }
for child in parent_node . children :
for deckId , childNode in self . collectDeckTreeChildren ( child ) . items ( ) :
allNodes [ deckId ] = childNode
return allNodes
2018-05-07 01:45:56 +00:00
#
# Miscellaneous
#
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 01:45:56 +00:00
def version ( self ) :
2020-01-05 23:42:08 +00:00
return util . setting ( ' apiVersion ' )
2018-05-07 01:45:56 +00:00
2022-04-14 04:00:52 +00:00
2021-05-08 03:33:06 +00:00
@util.api ( )
def requestPermission ( self , origin , allowed ) :
2022-04-14 04:00:52 +00:00
results = {
2021-05-08 03:33:06 +00:00
" permission " : " denied " ,
2022-04-14 04:00:52 +00:00
}
2021-07-14 00:55:22 +00:00
2022-04-14 04:00:52 +00:00
if allowed :
2021-05-08 03:33:06 +00:00
results = {
2022-04-14 04:00:52 +00:00
" permission " : " granted " ,
" requireApikey " : bool ( util . setting ( ' apiKey ' ) ) ,
" version " : util . setting ( ' apiVersion ' )
2021-05-08 03:33:06 +00:00
}
2022-04-14 04:00:52 +00:00
elif origin in util . setting ( ' ignoreOriginList ' ) :
pass # defaults to denied
else : # prompt the user
msg = QMessageBox ( None )
msg . setWindowTitle ( " A website wants to access to Anki " )
msg . setText ( ' " {} " requests permission to use Anki through AnkiConnect. Do you want to give it access? ' . format ( origin ) )
msg . setInformativeText ( " By granting permission, you ' ll allow the website to modify your collection on your behalf, including the execution of destructive actions such as deck deletion. " )
msg . setWindowIcon ( self . window ( ) . windowIcon ( ) )
2024-02-23 19:06:16 +00:00
msg . setIcon ( QMessageBox . Icon . Question )
msg . setStandardButtons ( QMessageBox . StandardButton . Yes | QMessageBox . StandardButton . No )
msg . setDefaultButton ( QMessageBox . StandardButton . No )
2022-04-14 04:00:52 +00:00
msg . setCheckBox ( QCheckBox ( text = ' Ignore further requests from " {} " ' . format ( origin ) , parent = msg ) )
2024-02-23 19:06:16 +00:00
if hasattr ( Qt , ' WindowStaysOnTopHint ' ) :
# Qt5
WindowOnTopFlag = Qt . WindowStaysOnTopHint
elif hasattr ( Qt , ' WindowType ' ) and hasattr ( Qt . WindowType , ' WindowStaysOnTopHint ' ) :
# Qt6
WindowOnTopFlag = Qt . WindowType . WindowStaysOnTopHint
msg . setWindowFlags ( WindowOnTopFlag )
pressedButton = msg . exec ( )
if pressedButton == QMessageBox . StandardButton . Yes :
2022-04-14 04:00:52 +00:00
config = aqt . mw . addonManager . getConfig ( __name__ )
config [ " webCorsOriginList " ] = util . setting ( ' webCorsOriginList ' )
config [ " webCorsOriginList " ] . append ( origin )
aqt . mw . addonManager . writeConfig ( __name__ , config )
results = {
" permission " : " granted " ,
" requireApikey " : bool ( util . setting ( ' apiKey ' ) ) ,
" version " : util . setting ( ' apiVersion ' )
}
# if the origin isn't an empty string, the user clicks "No", and the ignore box is checked
2024-02-23 19:06:16 +00:00
elif origin and pressedButton == QMessageBox . StandardButton . No and msg . checkBox ( ) . isChecked ( ) :
2022-04-14 04:00:52 +00:00
config = aqt . mw . addonManager . getConfig ( __name__ )
config [ " ignoreOriginList " ] = util . setting ( ' ignoreOriginList ' )
config [ " ignoreOriginList " ] . append ( origin )
aqt . mw . addonManager . writeConfig ( __name__ , config )
# else defaults to denied
2021-05-08 03:33:06 +00:00
return results
2021-01-18 06:13:27 +00:00
2020-05-01 14:24:51 +00:00
@util.api ( )
def getProfiles ( self ) :
return self . window ( ) . pm . profiles ( )
2024-07-02 13:27:19 +00:00
@util.api ( )
def getActiveProfile ( self ) :
return self . window ( ) . pm . name
2021-01-18 06:13:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-12-01 16:13:38 +00:00
def loadProfile ( self , name ) :
2018-12-02 03:42:56 +00:00
if name not in self . window ( ) . pm . profiles ( ) :
return False
2021-01-18 06:13:27 +00:00
if self . window ( ) . isVisible ( ) :
2018-12-01 16:13:38 +00:00
cur_profile = self . window ( ) . pm . name
if cur_profile != name :
self . window ( ) . unloadProfileAndShowProfileManager ( )
2021-02-08 01:40:44 +00:00
def waiter ( ) :
# This function waits until main window is closed
# It's needed cause sync can take quite some time
# And if we call loadProfile until sync is ended things will go wrong
if self . window ( ) . isVisible ( ) :
QTimer . singleShot ( 1000 , waiter )
else :
self . loadProfile ( name )
waiter ( )
2021-01-18 06:13:27 +00:00
else :
self . window ( ) . pm . load ( name )
self . window ( ) . loadProfile ( )
self . window ( ) . profileDiag . closeWithoutQuitting ( )
2018-12-02 03:42:56 +00:00
return True
2019-03-07 18:19:35 +00:00
2018-12-01 16:13:38 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 01:45:56 +00:00
def sync ( self ) :
2024-06-22 00:46:58 +00:00
mw = self . window ( )
auth = mw . pm . sync_auth ( )
if not auth :
raise Exception ( " sync: auth not configured " )
out = mw . col . sync_collection ( auth , mw . pm . media_syncing_enabled ( ) )
accepted_sync_statuses = [ out . NO_CHANGES , out . NORMAL_SYNC ]
if out . required not in accepted_sync_statuses :
raise Exception ( f " Sync status { out . required } not one of { accepted_sync_statuses } - see SyncCollectionResponse.ChangesRequired for list of sync statuses: https://github.com/ankitects/anki/blob/e41c4573d789afe8b020fab5d9d1eede50c3fa3d/proto/anki/sync.proto#L57-L65 " )
mw . onSync ( )
2018-05-07 01:45:56 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 01:45:56 +00:00
def multi ( self , actions ) :
2018-07-29 08:11:50 +00:00
return list ( map ( self . handler , actions ) )
2018-05-07 01:45:56 +00:00
2018-05-06 19:59:31 +00:00
2020-04-19 01:16:52 +00:00
@util.api ( )
def getNumCardsReviewedToday ( self ) :
return self . database ( ) . scalar ( ' select count() from revlog where id > ? ' , ( self . scheduler ( ) . dayCutoff - 86400 ) * 1000 )
2020-04-22 01:22:10 +00:00
2021-03-06 17:58:40 +00:00
@util.api ( )
def getNumCardsReviewedByDay ( self ) :
return self . database ( ) . all ( ' select date(id/1000 - ?, " unixepoch " , " localtime " ) as day, count() from revlog group by day order by day desc ' ,
int ( time . strftime ( " % H " , time . localtime ( self . scheduler ( ) . dayCutoff ) ) ) * 3600 )
2020-04-19 01:16:52 +00:00
2020-05-03 19:06:43 +00:00
@util.api ( )
2020-05-08 01:49:48 +00:00
def getCollectionStatsHTML ( self , wholeCollection = True ) :
stats = self . collection ( ) . stats ( )
stats . wholeCollection = wholeCollection
return stats . report ( )
2020-05-03 19:06:43 +00:00
2018-05-07 02:11:54 +00:00
#
# Decks
#
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def deckNames ( self ) :
2024-01-21 14:31:21 +00:00
return [ x . name for x in self . decks ( ) . all_names_and_ids ( ) ]
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def deckNamesAndIds ( self ) :
decks = { }
for deck in self . deckNames ( ) :
2018-05-07 18:02:51 +00:00
decks [ deck ] = self . decks ( ) . id ( deck )
2018-05-07 02:11:54 +00:00
return decks
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def getDecks ( self , cards ) :
decks = { }
for card in cards :
2018-05-07 21:18:20 +00:00
did = self . database ( ) . scalar ( ' select did from cards where id=? ' , card )
deck = self . decks ( ) . get ( did ) [ ' name ' ]
2018-05-07 02:11:54 +00:00
if deck in decks :
decks [ deck ] . append ( card )
else :
decks [ deck ] = [ card ]
return decks
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def createDeck ( self , deck ) :
2024-01-21 13:23:24 +00:00
self . startEditing ( )
return self . decks ( ) . id ( deck )
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def changeDeck ( self , cards , deck ) :
2018-05-09 01:47:45 +00:00
self . startEditing ( )
2018-05-07 02:11:54 +00:00
2018-05-09 01:47:45 +00:00
did = self . collection ( ) . decks . id ( deck )
2024-10-05 11:17:46 +00:00
mod = anki . utils . int_time ( )
2018-05-09 01:47:45 +00:00
usn = self . collection ( ) . usn ( )
2018-05-07 02:11:54 +00:00
2018-05-09 01:47:45 +00:00
# normal cards
scids = anki . utils . ids2str ( cards )
# remove any cards from filtered deck first
self . collection ( ) . sched . remFromDyn ( cards )
2018-05-07 02:11:54 +00:00
2018-05-09 01:47:45 +00:00
# then move into new deck
self . collection ( ) . db . execute ( ' update cards set usn=?, mod=?, did=? where id in ' + scids , usn , mod , did )
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def deleteDecks ( self , decks , cardsToo = False ) :
2022-03-30 18:32:39 +00:00
if not cardsToo :
# since f592672fa952260655881a75a2e3c921b2e23857 (2.1.28)
# (see anki$ git log "-Gassert cardsToo")
# you can't delete decks without deleting cards as well.
# however, since 62c23c6816adf912776b9378c008a52bb50b2e8d (2.1.45)
# passing cardsToo to `rem` (long deprecated) won't raise an error!
# this is dangerous, so let's raise our own exception
2022-04-13 23:42:13 +00:00
raise Exception ( " Since Anki 2.1.28 it ' s not possible "
" to delete decks without deleting cards as well " )
2024-01-21 13:23:24 +00:00
self . startEditing ( )
decks = filter ( lambda d : d in self . deckNames ( ) , decks )
for deck in decks :
did = self . decks ( ) . id ( deck )
2024-01-21 14:31:21 +00:00
self . decks ( ) . remove ( [ did ] )
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def getDeckConfig ( self , deck ) :
2021-01-18 06:13:27 +00:00
if deck not in self . deckNames ( ) :
2018-05-07 02:11:54 +00:00
return False
collection = self . collection ( )
did = collection . decks . id ( deck )
2024-01-21 14:31:21 +00:00
return collection . decks . config_dict_for_deck_id ( did )
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def saveDeckConfig ( self , config ) :
collection = self . collection ( )
config [ ' id ' ] = str ( config [ ' id ' ] )
2024-10-05 11:17:46 +00:00
config [ ' mod ' ] = anki . utils . int_time ( )
2018-05-07 02:11:54 +00:00
config [ ' usn ' ] = collection . usn ( )
2021-07-14 00:55:22 +00:00
if int ( config [ ' id ' ] ) not in [ c [ ' id ' ] for c in collection . decks . all_config ( ) ] :
return False
try :
collection . decks . save ( config )
2024-01-21 14:31:21 +00:00
collection . decks . update_config ( config )
2021-07-14 00:55:22 +00:00
except :
2018-05-07 02:11:54 +00:00
return False
return True
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def setDeckConfigId ( self , decks , configId ) :
2021-07-15 01:22:10 +00:00
configId = int ( configId )
2018-05-07 02:11:54 +00:00
for deck in decks :
if not deck in self . deckNames ( ) :
return False
collection = self . collection ( )
for deck in decks :
2021-07-15 01:22:10 +00:00
try :
did = str ( collection . decks . id ( deck ) )
deck_dict = aqt . mw . col . decks . decks [ did ]
deck_dict [ ' conf ' ] = configId
collection . decks . save ( deck_dict )
except :
return False
2018-05-07 02:11:54 +00:00
return True
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def cloneDeckConfigId ( self , name , cloneFrom = ' 1 ' ) :
2021-07-15 01:22:10 +00:00
configId = int ( cloneFrom )
collection = self . collection ( )
if configId not in [ c [ ' id ' ] for c in collection . decks . all_config ( ) ] :
2018-05-07 02:11:54 +00:00
return False
2024-01-21 14:31:21 +00:00
config = collection . decks . get_config ( configId )
return collection . decks . add_config_returning_id ( name , config )
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 02:11:54 +00:00
def removeDeckConfigId ( self , configId ) :
collection = self . collection ( )
2021-07-15 01:22:10 +00:00
if int ( configId ) not in [ c [ ' id ' ] for c in collection . decks . all_config ( ) ] :
2018-05-07 02:11:54 +00:00
return False
2024-01-21 14:31:21 +00:00
collection . decks . remove_config ( configId )
2018-05-07 02:11:54 +00:00
return True
2022-05-03 21:59:33 +00:00
@util.api ( )
def getDeckStats ( self , decks ) :
collection = self . collection ( )
scheduler = self . scheduler ( )
responseDict = { }
deckIds = list ( map ( lambda d : collection . decks . id ( d ) , decks ) )
allDeckNodes = self . collectDeckTreeChildren ( scheduler . deck_due_tree ( ) )
for deckId , deckNode in allDeckNodes . items ( ) :
if deckId in deckIds :
responseDict [ deckId ] = self . deckStatsToJson ( deckNode )
return responseDict
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2021-05-22 19:21:12 +00:00
def storeMediaFile ( self , filename , data = None , path = None , url = None , skipHash = None , deleteExisting = True ) :
if not ( data or path or url ) :
raise Exception ( ' You must provide a " data " , " path " , or " url " field. ' )
if data :
2021-02-20 18:04:25 +00:00
mediaData = base64 . b64decode ( data )
2020-12-12 16:41:01 +00:00
elif path :
2020-12-08 02:50:37 +00:00
with open ( path , ' rb ' ) as f :
2021-02-20 18:04:25 +00:00
mediaData = f . read ( )
2020-03-13 23:36:17 +00:00
elif url :
2021-02-20 18:04:25 +00:00
mediaData = util . download ( url )
2017-08-22 07:53:35 +00:00
2021-02-20 18:04:25 +00:00
if skipHash is None :
skip = False
else :
m = hashlib . md5 ( )
2021-02-27 05:16:39 +00:00
m . update ( mediaData )
2021-02-20 18:04:25 +00:00
skip = skipHash == m . hexdigest ( )
if skip :
return None
2022-06-06 08:17:22 +00:00
if deleteExisting :
self . deleteMediaFile ( filename )
2021-02-20 18:04:25 +00:00
return self . media ( ) . writeData ( filename , mediaData )
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-22 19:05:12 +00:00
def retrieveMediaFile ( self , filename ) :
filename = os . path . basename ( filename )
2020-01-05 23:42:08 +00:00
filename = unicodedata . normalize ( ' NFC ' , filename )
2017-08-22 19:05:12 +00:00
filename = self . media ( ) . stripIllegal ( filename )
2017-08-22 07:53:35 +00:00
2017-08-22 19:05:12 +00:00
path = os . path . join ( self . media ( ) . dir ( ) , filename )
if os . path . exists ( path ) :
with open ( path , ' rb ' ) as file :
return base64 . b64encode ( file . read ( ) ) . decode ( ' ascii ' )
2017-08-22 07:53:35 +00:00
return False
2021-04-12 04:26:03 +00:00
@util.api ( )
def getMediaFilesNames ( self , pattern = ' * ' ) :
path = os . path . join ( self . media ( ) . dir ( ) , pattern )
return [ os . path . basename ( p ) for p in glob . glob ( path ) ]
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-22 19:05:12 +00:00
def deleteMediaFile ( self , filename ) :
2024-01-21 14:34:14 +00:00
self . media ( ) . trash_files ( [ filename ] )
2017-08-22 07:53:35 +00:00
2023-03-21 16:46:55 +00:00
@util.api ( )
def getMediaDirPath ( self ) :
return os . path . abspath ( self . media ( ) . dir ( ) )
2017-08-22 07:53:35 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-06 19:59:31 +00:00
def addNote ( self , note ) :
2018-05-07 00:52:24 +00:00
ankiNote = self . createNote ( note )
2016-05-21 22:34:09 +00:00
2018-05-07 00:52:24 +00:00
collection = self . collection ( )
2016-07-17 16:38:33 +00:00
self . startEditing ( )
2019-03-05 02:53:57 +00:00
nCardsAdded = collection . addNote ( ankiNote )
if nCardsAdded < 1 :
raise Exception ( ' The field values you have provided would make an empty question on all cards. ' )
2016-05-21 22:34:09 +00:00
2018-05-07 00:52:24 +00:00
return ankiNote . id
2016-05-21 22:10:12 +00:00
2021-06-21 03:35:09 +00:00
def addMediaFromNote ( self , ankiNote , note ) :
audioObjectOrList = note . get ( ' audio ' )
self . addMedia ( ankiNote , audioObjectOrList , util . MediaType . Audio )
videoObjectOrList = note . get ( ' video ' )
self . addMedia ( ankiNote , videoObjectOrList , util . MediaType . Video )
pictureObjectOrList = note . get ( ' picture ' )
self . addMedia ( ankiNote , pictureObjectOrList , util . MediaType . Picture )
2020-12-06 05:24:50 +00:00
def addMedia ( self , ankiNote , mediaObjectOrList , mediaType ) :
if mediaObjectOrList is None :
2020-03-06 03:45:13 +00:00
return
2020-12-06 05:24:50 +00:00
if isinstance ( mediaObjectOrList , list ) :
mediaList = mediaObjectOrList
2020-03-06 03:45:13 +00:00
else :
2020-12-06 05:24:50 +00:00
mediaList = [ mediaObjectOrList ]
2020-03-06 03:45:13 +00:00
2020-12-06 05:24:50 +00:00
for media in mediaList :
if media is not None and len ( media [ ' fields ' ] ) > 0 :
2020-03-06 03:45:13 +00:00
try :
2021-02-20 18:04:25 +00:00
mediaFilename = self . storeMediaFile ( media [ ' filename ' ] ,
data = media . get ( ' data ' ) ,
path = media . get ( ' path ' ) ,
url = media . get ( ' url ' ) ,
2022-03-27 03:58:00 +00:00
skipHash = media . get ( ' skipHash ' ) ,
deleteExisting = media . get ( ' deleteExisting ' ) )
2020-03-06 03:45:13 +00:00
2021-02-20 18:04:25 +00:00
if mediaFilename is not None :
2020-12-06 05:24:50 +00:00
for field in media [ ' fields ' ] :
2020-02-23 10:59:19 +00:00
if field in ankiNote :
2020-12-06 05:24:50 +00:00
if mediaType is util . MediaType . Picture :
2021-06-03 21:36:04 +00:00
ankiNote [ field ] + = u ' <img src= " {} " > ' . format ( mediaFilename )
2020-12-06 05:24:50 +00:00
elif mediaType is util . MediaType . Audio or mediaType is util . MediaType . Video :
ankiNote [ field ] + = u ' [sound: {} ] ' . format ( mediaFilename )
2020-03-06 03:45:13 +00:00
except Exception as e :
errorMessage = str ( e ) . replace ( ' & ' , ' & ' ) . replace ( ' < ' , ' < ' ) . replace ( ' > ' , ' > ' )
2020-12-06 05:24:50 +00:00
for field in media [ ' fields ' ] :
2020-03-06 03:45:13 +00:00
if field in ankiNote :
ankiNote [ field ] + = errorMessage
2020-02-23 10:59:19 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-02-19 20:07:10 +00:00
def canAddNote ( self , note ) :
2018-03-16 12:51:48 +00:00
try :
2018-05-07 00:52:24 +00:00
return bool ( self . createNote ( note ) )
2018-03-16 12:51:48 +00:00
except :
return False
2016-05-21 22:10:12 +00:00
2024-02-23 05:39:35 +00:00
@util.api ( )
def canAddNoteWithErrorDetail ( self , note ) :
try :
return {
' canAdd ' : bool ( self . createNote ( note ) )
}
except Exception as e :
return {
' canAdd ' : False ,
' error ' : str ( e )
}
2016-05-21 22:10:12 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-08 22:13:49 +00:00
def updateNoteFields ( self , note ) :
2021-03-05 04:06:25 +00:00
ankiNote = self . getNote ( note [ ' id ' ] )
2018-05-07 00:52:24 +00:00
2021-09-16 00:55:46 +00:00
self . startEditing ( )
2018-05-08 22:13:49 +00:00
for name , value in note [ ' fields ' ] . items ( ) :
if name in ankiNote :
ankiNote [ name ] = value
2018-05-07 00:52:24 +00:00
2024-10-07 19:54:40 +00:00
self . addMediaFromNote ( ankiNote , note )
2020-02-23 10:59:19 +00:00
2024-10-05 11:26:28 +00:00
self . collection ( ) . update_note ( ankiNote , skip_undo_entry = True ) ;
2016-05-21 22:10:12 +00:00
2021-01-18 06:13:27 +00:00
2022-12-30 11:13:31 +00:00
@util.api ( )
def updateNote ( self , note ) :
updated = False
if ' fields ' in note . keys ( ) :
self . updateNoteFields ( note )
updated = True
if ' tags ' in note . keys ( ) :
self . updateNoteTags ( note [ ' id ' ] , note [ ' tags ' ] )
updated = True
if not updated :
raise Exception ( ' Must provide a " fields " or " tags " property. ' )
2024-05-09 12:29:18 +00:00
@util.api ( )
def updateNoteModel ( self , note ) :
"""
Update the model and fields of a given note .
: param note : A dictionary containing note details , including ' id ' , ' modelName ' , ' fields ' , and ' tags ' .
"""
2024-05-10 04:16:12 +00:00
# Extract and validate the note ID
note_id = note . get ( ' id ' )
if not note_id :
raise ValueError ( " Note ID is required " )
2024-05-09 12:29:18 +00:00
2024-05-10 04:16:12 +00:00
# Extract and validate the new model name
new_model_name = note . get ( ' modelName ' )
if not new_model_name :
raise ValueError ( " Model name is required " )
2024-05-09 12:29:18 +00:00
2024-05-10 04:16:12 +00:00
# Extract and validate the new fields
new_fields = note . get ( ' fields ' )
if not new_fields or not isinstance ( new_fields , dict ) :
raise ValueError ( " Fields must be provided as a dictionary " )
2024-05-09 12:29:18 +00:00
2024-05-10 04:16:12 +00:00
# Extract the new tags
new_tags = note . get ( ' tags ' , [ ] )
2024-05-09 12:29:18 +00:00
2024-05-10 04:16:12 +00:00
# Get the current note from the collection
anki_note = self . getNote ( note_id )
# Get the new model from the collection
collection = self . collection ( )
new_model = collection . models . by_name ( new_model_name )
if not new_model :
raise ValueError ( f " Model ' { new_model_name } ' not found " )
# Update the note's model
anki_note . mid = new_model [ ' id ' ]
anki_note . _fmap = collection . models . field_map ( new_model )
anki_note . fields = [ ' ' ] * len ( new_model [ ' flds ' ] )
# Update the fields with new values
for name , value in new_fields . items ( ) :
for anki_name in anki_note . keys ( ) :
if name . lower ( ) == anki_name . lower ( ) :
anki_note [ anki_name ] = value
break
# Update the tags
anki_note . tags = new_tags
2024-10-05 11:26:28 +00:00
# Update note to ensure changes are saved
collection . update_note ( anki_note , skip_undo_entry = True ) ;
2024-05-10 04:16:12 +00:00
2022-12-30 11:13:31 +00:00
@util.api ( )
def updateNoteTags ( self , note , tags ) :
if type ( tags ) == str :
tags = [ tags ]
if type ( tags ) != list or not all ( [ type ( t ) == str for t in tags ] ) :
raise Exception ( ' Must provide tags as a list of strings ' )
for old_tag in self . getNoteTags ( note ) :
self . removeTags ( [ note ] , old_tag )
for new_tag in tags :
self . addTags ( [ note ] , new_tag )
@util.api ( )
def getNoteTags ( self , note ) :
return self . getNote ( note ) . tags
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-03 21:21:59 +00:00
def addTags ( self , notes , tags , add = True ) :
2017-08-05 08:24:03 +00:00
self . startEditing ( )
2017-08-06 01:29:59 +00:00
self . collection ( ) . tags . bulkAdd ( notes , tags , add )
2017-08-03 20:07:22 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-06 19:59:31 +00:00
def removeTags ( self , notes , tags ) :
return self . addTags ( notes , tags , False )
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-01-14 11:26:37 +00:00
def getTags ( self ) :
return self . collection ( ) . tags . all ( )
2021-01-02 17:05:35 +00:00
2021-01-18 06:13:27 +00:00
2021-01-02 17:05:35 +00:00
@util.api ( )
def clearUnusedTags ( self ) :
2021-01-18 06:13:27 +00:00
self . collection ( ) . tags . registerNotes ( )
2021-01-02 17:05:35 +00:00
@util.api ( )
def replaceTags ( self , notes , tag_to_replace , replace_with_tag ) :
2021-01-18 06:13:27 +00:00
self . window ( ) . progress . start ( )
for nid in notes :
2021-03-05 04:06:25 +00:00
try :
note = self . getNote ( nid )
except NotFoundError :
continue
2024-01-21 14:31:21 +00:00
if note . has_tag ( tag_to_replace ) :
note . remove_tag ( tag_to_replace )
note . add_tag ( replace_with_tag )
2024-10-05 11:26:28 +00:00
self . collection ( ) . update_note ( note , skip_undo_entry = True ) ;
2021-01-18 06:13:27 +00:00
self . window ( ) . requireReset ( )
self . window ( ) . progress . finish ( )
self . window ( ) . reset ( )
2021-01-02 17:05:35 +00:00
@util.api ( )
def replaceTagsInAllNotes ( self , tag_to_replace , replace_with_tag ) :
2021-01-18 06:13:27 +00:00
self . window ( ) . progress . start ( )
2021-03-05 04:06:25 +00:00
collection = self . collection ( )
2021-01-18 06:13:27 +00:00
for nid in collection . db . list ( ' select id from notes ' ) :
2021-03-05 04:06:25 +00:00
note = self . getNote ( nid )
2024-01-21 14:31:21 +00:00
if note . has_tag ( tag_to_replace ) :
note . remove_tag ( tag_to_replace )
note . add_tag ( replace_with_tag )
2024-10-05 11:26:28 +00:00
self . collection ( ) . update_note ( note , skip_undo_entry = True ) ;
2021-01-18 06:13:27 +00:00
self . window ( ) . requireReset ( )
self . window ( ) . progress . finish ( )
self . window ( ) . reset ( )
2020-06-10 23:21:58 +00:00
@util.api ( )
def setEaseFactors ( self , cards , easeFactors ) :
couldSetEaseFactors = [ ]
2021-01-18 06:13:27 +00:00
for i , card in enumerate ( cards ) :
2021-03-05 04:06:25 +00:00
try :
ankiCard = self . getCard ( card )
except NotFoundError :
2020-06-10 23:21:58 +00:00
couldSetEaseFactors . append ( False )
2021-03-05 04:06:25 +00:00
continue
2020-06-10 23:21:58 +00:00
2021-03-05 04:06:25 +00:00
couldSetEaseFactors . append ( True )
2021-01-19 05:03:27 +00:00
ankiCard . factor = easeFactors [ i ]
2024-10-05 11:26:28 +00:00
self . collection ( ) . update_card ( ankiCard , skip_undo_entry = True )
2020-06-10 23:21:58 +00:00
return couldSetEaseFactors
2021-01-18 06:13:27 +00:00
2022-02-23 02:31:43 +00:00
@util.api ( )
def setSpecificValueOfCard ( self , card , keys ,
newValues , warning_check = False ) :
if isinstance ( card , list ) :
print ( " card has to be int, not list " )
return False
if not isinstance ( keys , list ) or not isinstance ( newValues , list ) :
print ( " keys and newValues have to be lists. " )
return False
if len ( newValues ) != len ( keys ) :
print ( " Invalid list lengths. " )
return False
for key in keys :
if key in [ " did " , " id " , " ivl " , " lapses " , " left " , " mod " , " nid " ,
" odid " , " odue " , " ord " , " queue " , " reps " , " type " , " usn " ] :
if warning_check is False :
return False
result = [ ]
try :
ankiCard = self . getCard ( card )
for i , key in enumerate ( keys ) :
setattr ( ankiCard , key , newValues [ i ] )
2024-10-05 11:26:28 +00:00
self . collection ( ) . update_card ( ankiCard , skip_undo_entry = True )
2022-02-23 02:31:43 +00:00
result . append ( True )
except Exception as e :
result . append ( [ False , str ( e ) ] )
return result
2021-01-18 06:13:27 +00:00
2020-06-10 23:21:58 +00:00
@util.api ( )
def getEaseFactors ( self , cards ) :
easeFactors = [ ]
for card in cards :
2021-03-05 04:06:25 +00:00
try :
ankiCard = self . getCard ( card )
except NotFoundError :
easeFactors . append ( None )
continue
2020-06-10 23:21:58 +00:00
easeFactors . append ( ankiCard . factor )
2018-01-14 11:26:37 +00:00
2020-06-10 23:21:58 +00:00
return easeFactors
2018-01-14 11:26:37 +00:00
2021-01-18 06:13:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-03 21:21:59 +00:00
def suspend ( self , cards , suspend = True ) :
2017-08-06 01:29:59 +00:00
for card in cards :
2018-05-07 00:52:24 +00:00
if self . suspended ( card ) == suspend :
2017-08-06 01:29:59 +00:00
cards . remove ( card )
2018-05-07 00:52:24 +00:00
if len ( cards ) == 0 :
return False
2017-08-06 01:29:59 +00:00
2018-05-07 00:52:24 +00:00
scheduler = self . scheduler ( )
self . startEditing ( )
if suspend :
scheduler . suspendCards ( cards )
else :
scheduler . unsuspendCards ( cards )
return True
2017-08-06 01:29:59 +00:00
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-06 19:59:31 +00:00
def unsuspend ( self , cards ) :
self . suspend ( cards , False )
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-07 00:52:24 +00:00
def suspended ( self , card ) :
2021-03-05 04:06:25 +00:00
card = self . getCard ( card )
2018-05-06 19:59:31 +00:00
return card . queue == - 1
2017-08-06 01:29:59 +00:00
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-12 14:57:28 +00:00
def areSuspended ( self , cards ) :
suspended = [ ]
for card in cards :
2021-03-05 04:06:25 +00:00
try :
suspended . append ( self . suspended ( card ) )
except NotFoundError :
suspended . append ( None )
2018-05-07 00:52:24 +00:00
2017-08-12 14:57:28 +00:00
return suspended
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-12 14:57:28 +00:00
def areDue ( self , cards ) :
due = [ ]
for card in cards :
2018-05-07 00:52:24 +00:00
if self . findCards ( ' cid: {} is:new ' . format ( card ) ) :
2017-08-12 14:57:28 +00:00
due . append ( True )
else :
2018-05-07 00:52:24 +00:00
date , ivl = self . collection ( ) . db . all ( ' select id/1000.0, ivl from revlog where cid = ? ' , card ) [ - 1 ]
if ivl > = - 1200 :
2018-06-03 15:26:33 +00:00
due . append ( bool ( self . findCards ( ' cid: {} is:due ' . format ( card ) ) ) )
2017-08-12 14:57:28 +00:00
else :
2020-01-05 23:42:08 +00:00
due . append ( date - ivl < = time . time ( ) )
2017-08-12 14:57:28 +00:00
return due
2017-08-03 20:07:22 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-09 17:40:09 +00:00
def getIntervals ( self , cards , complete = False ) :
intervals = [ ]
for card in cards :
2018-05-07 00:52:24 +00:00
if self . findCards ( ' cid: {} is:new ' . format ( card ) ) :
2017-08-12 15:21:04 +00:00
intervals . append ( 0 )
2018-05-07 00:52:24 +00:00
else :
interval = self . collection ( ) . db . list ( ' select ivl from revlog where cid = ? ' , card )
if not complete :
interval = interval [ - 1 ]
intervals . append ( interval )
2017-08-12 15:21:04 +00:00
2017-08-09 17:40:09 +00:00
return intervals
2017-08-17 12:25:06 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2016-05-21 22:10:12 +00:00
def modelNames ( self ) :
2024-01-21 14:31:21 +00:00
return [ n . name for n in self . collection ( ) . models . all_names_and_ids ( ) ]
2016-05-21 22:10:12 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2021-05-07 02:11:03 +00:00
def createModel ( self , modelName , inOrderFields , cardTemplates , css = None , isCloze = False ) :
2019-02-22 18:44:51 +00:00
# https://github.com/dae/anki/blob/b06b70f7214fb1f2ce33ba06d2b095384b81f874/anki/stdmodels.py
2021-01-18 06:13:27 +00:00
if len ( inOrderFields ) == 0 :
2019-03-01 17:38:30 +00:00
raise Exception ( ' Must provide at least one field for inOrderFields ' )
2021-01-18 06:13:27 +00:00
if len ( cardTemplates ) == 0 :
2019-03-01 17:38:30 +00:00
raise Exception ( ' Must provide at least one card for cardTemplates ' )
2024-01-21 14:31:21 +00:00
if modelName in [ n . name for n in self . collection ( ) . models . all_names_and_ids ( ) ] :
2019-03-01 17:38:30 +00:00
raise Exception ( ' Model name already exists ' )
2019-02-22 18:44:51 +00:00
collection = self . collection ( )
mm = collection . models
# Generate new Note
2020-06-03 03:27:33 +00:00
m = mm . new ( modelName )
2021-05-07 02:11:03 +00:00
if isCloze :
m [ ' type ' ] = MODEL_CLOZE
2019-02-22 18:44:51 +00:00
# Create fields and add them to Note
for field in inOrderFields :
2024-01-21 14:31:21 +00:00
fm = mm . new_field ( field )
2019-02-22 18:44:51 +00:00
mm . addField ( m , fm )
2019-03-07 18:19:35 +00:00
2019-03-01 17:38:30 +00:00
# Add shared css to model if exists. Use default otherwise
if ( css is not None ) :
m [ ' css ' ] = css
2019-02-22 18:44:51 +00:00
# Generate new card template(s)
cardCount = 1
for card in cardTemplates :
2020-03-13 22:40:27 +00:00
cardName = ' Card ' + str ( cardCount )
if ' Name ' in card :
cardName = card [ ' Name ' ]
2024-01-21 14:31:21 +00:00
t = mm . new_template ( cardName )
2019-02-22 18:44:51 +00:00
cardCount + = 1
t [ ' qfmt ' ] = card [ ' Front ' ]
t [ ' afmt ' ] = card [ ' Back ' ]
mm . addTemplate ( m , t )
mm . add ( m )
return m
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-21 16:15:11 +00:00
def modelNamesAndIds ( self ) :
models = { }
2018-05-07 00:52:24 +00:00
for model in self . modelNames ( ) :
2024-01-21 14:31:21 +00:00
models [ model ] = int ( self . collection ( ) . models . by_name ( model ) [ ' id ' ] )
2017-08-21 16:15:11 +00:00
return models
2024-01-04 16:49:17 +00:00
@util.api ( )
def findModelsById ( self , modelIds ) :
models = [ ]
for id in modelIds :
model = self . collection ( ) . models . get ( id )
if model is None :
raise Exception ( " model was not found: {} " . format ( id ) )
else :
models . append ( model )
return models
@util.api ( )
def findModelsByName ( self , modelNames ) :
models = [ ]
for name in modelNames :
2024-01-21 14:31:21 +00:00
model = self . collection ( ) . models . by_name ( name )
2024-01-04 16:49:17 +00:00
if model is None :
raise Exception ( " model was not found: {} " . format ( name ) )
else :
models . append ( model )
return models
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-07-01 19:49:44 +00:00
def modelNameFromId ( self , modelId ) :
2018-05-07 00:52:24 +00:00
model = self . collection ( ) . models . get ( modelId )
if model is None :
raise Exception ( ' model was not found: {} ' . format ( modelId ) )
else :
return model [ ' name ' ]
2016-05-29 17:31:24 +00:00
2017-07-01 19:41:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-07-01 19:41:27 +00:00
def modelFieldNames ( self , modelName ) :
2024-01-21 14:31:21 +00:00
model = self . collection ( ) . models . by_name ( modelName )
2018-05-07 00:52:24 +00:00
if model is None :
raise Exception ( ' model was not found: {} ' . format ( modelName ) )
else :
return [ field [ ' name ' ] for field in model [ ' flds ' ] ]
2016-05-21 22:10:12 +00:00
2022-11-26 12:56:34 +00:00
@util.api ( )
def modelFieldDescriptions ( self , modelName ) :
2024-01-21 14:31:21 +00:00
model = self . collection ( ) . models . by_name ( modelName )
2022-11-26 12:56:34 +00:00
if model is None :
raise Exception ( ' model was not found: {} ' . format ( modelName ) )
else :
2022-11-27 10:19:58 +00:00
try :
return [ field [ ' description ' ] for field in model [ ' flds ' ] ]
except KeyError :
# older versions of Anki don't have field descriptions
return [ ' ' for field in model [ ' flds ' ] ]
2022-11-26 12:56:34 +00:00
2022-12-24 11:10:56 +00:00
@util.api ( )
def modelFieldFonts ( self , modelName ) :
model = self . getModel ( modelName )
2022-12-29 01:37:05 +00:00
fonts = { }
2022-12-24 11:10:56 +00:00
for field in model [ ' flds ' ] :
2022-12-29 01:37:05 +00:00
fonts [ field [ ' name ' ] ] = {
' font ' : field [ ' font ' ] ,
' size ' : field [ ' size ' ] ,
2022-12-24 11:10:56 +00:00
}
return fonts
2022-11-26 12:56:34 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-21 14:10:57 +00:00
def modelFieldsOnTemplates ( self , modelName ) :
2024-01-21 14:31:21 +00:00
model = self . collection ( ) . models . by_name ( modelName )
2018-05-07 00:52:24 +00:00
if model is None :
raise Exception ( ' model was not found: {} ' . format ( modelName ) )
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
templates = { }
for template in model [ ' tmpls ' ] :
fields = [ ]
for side in [ ' qfmt ' , ' afmt ' ] :
fieldsForSide = [ ]
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
# based on _fieldsOnTemplate from aqt/clayout.py
matches = re . findall ( ' {{ [^#/}]+?}} ' , template [ side ] )
for match in matches :
# remove braces and modifiers
match = re . sub ( r ' [ {} ] ' , ' ' , match )
match = match . split ( ' : ' ) [ - 1 ]
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
# for the answer side, ignore fields present on the question side + the FrontSide field
if match == ' FrontSide ' or side == ' afmt ' and match in fields [ 0 ] :
continue
fieldsForSide . append ( match )
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
fields . append ( fieldsForSide )
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
templates [ template [ ' name ' ] ] = fields
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
return templates
2017-08-21 14:10:57 +00:00
2021-01-18 06:13:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2019-10-27 20:41:53 +00:00
def modelTemplates ( self , modelName ) :
2024-01-21 14:31:21 +00:00
model = self . collection ( ) . models . by_name ( modelName )
2019-10-27 20:41:53 +00:00
if model is None :
raise Exception ( ' model was not found: {} ' . format ( modelName ) )
templates = { }
for template in model [ ' tmpls ' ] :
templates [ template [ ' name ' ] ] = { ' Front ' : template [ ' qfmt ' ] , ' Back ' : template [ ' afmt ' ] }
return templates
2020-01-05 23:42:08 +00:00
@util.api ( )
2019-10-27 20:41:53 +00:00
def modelStyling ( self , modelName ) :
2024-01-21 14:31:21 +00:00
model = self . collection ( ) . models . by_name ( modelName )
2019-10-27 20:41:53 +00:00
if model is None :
raise Exception ( ' model was not found: {} ' . format ( modelName ) )
return { ' css ' : model [ ' css ' ] }
2020-01-05 23:42:08 +00:00
@util.api ( )
2019-10-27 20:41:53 +00:00
def updateModelTemplates ( self , model ) :
models = self . collection ( ) . models
2024-01-21 14:31:21 +00:00
ankiModel = models . by_name ( model [ ' name ' ] )
2019-10-27 20:41:53 +00:00
if ankiModel is None :
raise Exception ( ' model was not found: {} ' . format ( model [ ' name ' ] ) )
templates = model [ ' templates ' ]
for ankiTemplate in ankiModel [ ' tmpls ' ] :
template = templates . get ( ankiTemplate [ ' name ' ] )
if template :
qfmt = template . get ( ' Front ' )
if qfmt :
ankiTemplate [ ' qfmt ' ] = qfmt
afmt = template . get ( ' Back ' )
if afmt :
ankiTemplate [ ' afmt ' ] = afmt
2021-12-27 06:21:21 +00:00
self . save_model ( models , ankiModel )
2019-10-27 20:41:53 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2019-10-27 20:41:53 +00:00
def updateModelStyling ( self , model ) :
models = self . collection ( ) . models
2024-01-21 14:31:21 +00:00
ankiModel = models . by_name ( model [ ' name ' ] )
2019-10-27 20:41:53 +00:00
if ankiModel is None :
raise Exception ( ' model was not found: {} ' . format ( model [ ' name ' ] ) )
ankiModel [ ' css ' ] = model [ ' css ' ]
2021-12-27 06:21:21 +00:00
self . save_model ( models , ankiModel )
2019-10-27 20:41:53 +00:00
2017-08-21 14:10:57 +00:00
2021-02-27 05:17:51 +00:00
@util.api ( )
def findAndReplaceInModels ( self , modelName , findText , replaceText , front = True , back = True , css = True ) :
if not modelName :
ankiModel = self . collection ( ) . models . allNames ( )
else :
2024-01-21 14:31:21 +00:00
model = self . collection ( ) . models . by_name ( modelName )
2021-02-27 05:17:51 +00:00
if model is None :
raise Exception ( ' model was not found: {} ' . format ( modelName ) )
2022-03-30 18:34:58 +00:00
ankiModel = [ modelName ]
2021-02-27 05:17:51 +00:00
updatedModels = 0
for model in ankiModel :
2024-01-21 14:31:21 +00:00
model = self . collection ( ) . models . by_name ( model )
2021-02-27 05:17:51 +00:00
checkForText = False
if css and findText in model [ ' css ' ] :
checkForText = True
2021-07-14 00:55:22 +00:00
model [ ' css ' ] = model [ ' css ' ] . replace ( findText , replaceText )
2021-02-27 05:17:51 +00:00
for tmpls in model . get ( ' tmpls ' ) :
if front and findText in tmpls [ ' qfmt ' ] :
checkForText = True
tmpls [ ' qfmt ' ] = tmpls [ ' qfmt ' ] . replace ( findText , replaceText )
if back and findText in tmpls [ ' afmt ' ] :
checkForText = True
2021-07-14 00:55:22 +00:00
tmpls [ ' afmt ' ] = tmpls [ ' afmt ' ] . replace ( findText , replaceText )
2021-12-27 06:21:21 +00:00
self . save_model ( self . collection ( ) . models , model )
2021-02-27 05:17:51 +00:00
if checkForText :
updatedModels + = 1
return updatedModels
2023-04-14 01:30:49 +00:00
@util.api ( )
def modelTemplateRename ( self , modelName , oldTemplateName , newTemplateName ) :
mm = self . collection ( ) . models
model = self . getModel ( modelName )
ankiTemplate = self . getTemplate ( model , oldTemplateName )
ankiTemplate [ ' name ' ] = newTemplateName
self . save_model ( mm , model )
@util.api ( )
def modelTemplateReposition ( self , modelName , templateName , index ) :
mm = self . collection ( ) . models
model = self . getModel ( modelName )
ankiTemplate = self . getTemplate ( model , templateName )
mm . reposition_template ( model , ankiTemplate , index )
self . save_model ( mm , model )
@util.api ( )
def modelTemplateAdd ( self , modelName , template ) :
# "Name", "Front", "Back" borrows from `createModel`
mm = self . collection ( ) . models
model = self . getModel ( modelName )
name = template [ ' Name ' ]
qfmt = template [ ' Front ' ]
afmt = template [ ' Back ' ]
# updates the template if it already exists
for ankiTemplate in model [ ' tmpls ' ] :
if ankiTemplate [ ' name ' ] == name :
ankiTemplate [ ' qfmt ' ] = qfmt
ankiTemplate [ ' afmt ' ] = afmt
return
ankiTemplate = mm . new_template ( name )
ankiTemplate [ ' qfmt ' ] = qfmt
ankiTemplate [ ' afmt ' ] = afmt
mm . add_template ( model , ankiTemplate )
self . save_model ( mm , model )
@util.api ( )
def modelTemplateRemove ( self , modelName , templateName ) :
mm = self . collection ( ) . models
model = self . getModel ( modelName )
ankiTemplate = self . getTemplate ( model , templateName )
mm . remove_template ( model , ankiTemplate )
self . save_model ( mm , model )
2022-08-17 01:23:09 +00:00
@util.api ( )
def modelFieldRename ( self , modelName , oldFieldName , newFieldName ) :
2022-12-24 11:10:56 +00:00
mm = self . collection ( ) . models
model = self . getModel ( modelName )
2023-04-14 01:30:49 +00:00
field = self . getField ( model , oldFieldName )
2022-08-17 01:23:09 +00:00
mm . renameField ( model , field , newFieldName )
self . save_model ( mm , model )
@util.api ( )
def modelFieldReposition ( self , modelName , fieldName , index ) :
2022-12-24 11:10:56 +00:00
mm = self . collection ( ) . models
model = self . getModel ( modelName )
2023-04-14 01:30:49 +00:00
field = self . getField ( model , fieldName )
2022-08-17 01:23:09 +00:00
2024-01-21 14:31:21 +00:00
mm . reposition_field ( model , field , index )
2022-08-17 01:23:09 +00:00
self . save_model ( mm , model )
@util.api ( )
def modelFieldAdd ( self , modelName , fieldName , index = None ) :
mm = self . collection ( ) . models
2022-12-24 11:10:56 +00:00
model = self . getModel ( modelName )
2022-08-17 01:23:09 +00:00
# only adds the field if it doesn't already exist
2024-01-21 14:31:21 +00:00
fieldMap = mm . field_map ( model )
2022-08-17 01:23:09 +00:00
if fieldName not in fieldMap :
2024-01-21 14:31:21 +00:00
field = mm . new_field ( fieldName )
2022-08-17 01:23:09 +00:00
mm . addField ( model , field )
# repositions, even if the field already exists
if index is not None :
2024-01-21 14:31:21 +00:00
fieldMap = mm . field_map ( model )
2022-08-17 01:23:09 +00:00
newField = fieldMap [ fieldName ] [ 1 ]
2024-01-21 14:31:21 +00:00
mm . reposition_field ( model , newField , index )
2022-08-17 01:23:09 +00:00
self . save_model ( mm , model )
@util.api ( )
def modelFieldRemove ( self , modelName , fieldName ) :
2022-12-24 11:10:56 +00:00
mm = self . collection ( ) . models
model = self . getModel ( modelName )
2023-04-14 01:30:49 +00:00
field = self . getField ( model , fieldName )
2022-08-17 01:23:09 +00:00
2024-01-21 14:31:21 +00:00
mm . remove_field ( model , field )
2022-08-17 01:23:09 +00:00
self . save_model ( mm , model )
2022-12-24 11:10:56 +00:00
@util.api ( )
def modelFieldSetFont ( self , modelName , fieldName , font ) :
mm = self . collection ( ) . models
model = self . getModel ( modelName )
2023-04-14 01:30:49 +00:00
field = self . getField ( model , fieldName )
2022-12-24 11:10:56 +00:00
if not isinstance ( font , str ) :
2022-12-24 11:18:24 +00:00
raise Exception ( ' font should be a string: {} ' . format ( font ) )
2022-12-24 11:10:56 +00:00
2022-12-24 11:18:24 +00:00
field [ ' font ' ] = font
self . save_model ( mm , model )
@util.api ( )
2022-12-29 01:37:05 +00:00
def modelFieldSetFontSize ( self , modelName , fieldName , fontSize ) :
2022-12-24 11:18:24 +00:00
mm = self . collection ( ) . models
model = self . getModel ( modelName )
2023-04-14 01:30:49 +00:00
field = self . getField ( model , fieldName )
2022-12-24 11:18:24 +00:00
2022-12-29 01:37:05 +00:00
if not isinstance ( fontSize , int ) :
raise Exception ( ' fontSize should be an integer: {} ' . format ( fontSize ) )
2022-12-24 11:18:24 +00:00
2022-12-29 01:37:05 +00:00
field [ ' size ' ] = fontSize
2022-12-24 11:10:56 +00:00
self . save_model ( mm , model )
@util.api ( )
2022-12-29 01:37:05 +00:00
def modelFieldSetDescription ( self , modelName , fieldName , description ) :
2022-12-24 11:10:56 +00:00
mm = self . collection ( ) . models
model = self . getModel ( modelName )
2023-04-14 01:30:49 +00:00
field = self . getField ( model , fieldName )
2022-12-24 11:10:56 +00:00
2022-12-29 01:37:05 +00:00
if not isinstance ( description , str ) :
raise Exception ( ' description should be a string: {} ' . format ( description ) )
2022-12-24 11:10:56 +00:00
2022-12-29 01:37:05 +00:00
if ' description ' in field : # older versions do not have the 'description' key
field [ ' description ' ] = description
self . save_model ( mm , model )
return True
return False
2022-12-24 11:10:56 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-07-01 19:49:44 +00:00
def deckNameFromId ( self , deckId ) :
2018-05-07 00:52:24 +00:00
deck = self . collection ( ) . decks . get ( deckId )
if deck is None :
raise Exception ( ' deck was not found: {} ' . format ( deckId ) )
2021-01-18 06:13:27 +00:00
return deck [ ' name ' ]
2017-07-01 19:41:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-03 21:31:47 +00:00
def findNotes ( self , query = None ) :
2018-05-07 00:52:24 +00:00
if query is None :
2017-08-03 21:31:47 +00:00
return [ ]
2021-01-18 06:13:27 +00:00
2024-01-21 14:31:21 +00:00
return list ( map ( int , self . collection ( ) . find_notes ( query ) ) )
2017-08-03 21:31:47 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-03 21:31:47 +00:00
def findCards ( self , query = None ) :
2018-05-07 01:45:56 +00:00
if query is None :
2017-08-03 21:31:47 +00:00
return [ ]
2021-01-18 06:13:27 +00:00
2024-01-21 14:31:21 +00:00
return list ( map ( int , self . collection ( ) . find_cards ( query ) ) )
2017-08-03 21:31:47 +00:00
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-06 19:59:31 +00:00
def cardsInfo ( self , cards ) :
2018-01-06 19:48:25 +00:00
result = [ ]
for cid in cards :
try :
2021-03-05 04:06:25 +00:00
card = self . getCard ( cid )
2024-01-21 14:31:21 +00:00
model = card . note_type ( )
2018-01-06 19:48:25 +00:00
note = card . note ( )
fields = { }
for info in model [ ' flds ' ] :
order = info [ ' ord ' ]
name = info [ ' name ' ]
2018-01-09 13:09:38 +00:00
fields [ name ] = { ' value ' : note . fields [ order ] , ' order ' : order }
2024-06-08 23:29:17 +00:00
states = self . collection ( ) . _backend . get_scheduling_states ( card . id )
nextReviews = self . collection ( ) . _backend . describe_next_states ( states )
2018-03-31 21:40:01 +00:00
2018-01-06 19:48:25 +00:00
result . append ( {
' cardId ' : card . id ,
' fields ' : fields ,
' fieldOrder ' : card . ord ,
2021-01-18 06:13:27 +00:00
' question ' : util . cardQuestion ( card ) ,
' answer ' : util . cardAnswer ( card ) ,
2018-01-06 19:48:25 +00:00
' modelName ' : model [ ' name ' ] ,
2020-07-12 19:53:31 +00:00
' ord ' : card . ord ,
2018-01-06 19:48:25 +00:00
' deckName ' : self . deckNameFromId ( card . did ) ,
' css ' : model [ ' css ' ] ,
2018-03-31 21:40:01 +00:00
' factor ' : card . factor ,
#This factor is 10 times the ease percentage,
2018-01-06 19:48:25 +00:00
# so an ease of 310% would be reported as 3100
' interval ' : card . ivl ,
2020-07-12 19:53:31 +00:00
' note ' : card . nid ,
' type ' : card . type ,
' queue ' : card . queue ,
' due ' : card . due ,
' reps ' : card . reps ,
' lapses ' : card . lapses ,
' left ' : card . left ,
2021-08-25 03:55:23 +00:00
' mod ' : card . mod ,
2024-06-08 23:29:17 +00:00
' nextReviews ' : list ( nextReviews ) ,
2024-11-05 19:58:44 +00:00
' flags ' : card . flags ,
2018-01-06 19:48:25 +00:00
} )
2021-03-05 04:06:25 +00:00
except NotFoundError :
# Anki will give a NotFoundError if the card ID does not exist.
2018-03-11 22:10:07 +00:00
# Best behavior is probably to add an 'empty card' to the
2018-01-06 19:48:25 +00:00
# returned result, so that the items of the input and return
# lists correspond.
result . append ( { } )
return result
2021-08-25 03:55:23 +00:00
@util.api ( )
def cardsModTime ( self , cards ) :
result = [ ]
for cid in cards :
try :
card = self . getCard ( cid )
result . append ( {
' cardId ' : card . id ,
' mod ' : card . mod ,
} )
except NotFoundError :
# Anki will give a NotFoundError if the card ID does not exist.
# Best behavior is probably to add an 'empty card' to the
# returned result, so that the items of the input and return
# lists correspond.
result . append ( { } )
return result
2021-03-05 04:02:57 +00:00
@util.api ( )
def forgetCards ( self , cards ) :
self . startEditing ( )
2024-02-18 19:45:10 +00:00
request = ScheduleCardsAsNew (
card_ids = cards ,
log = True ,
restore_position = True ,
reset_counts = False ,
context = None ,
)
self . collection ( ) . _backend . schedule_cards_as_new ( request )
2021-03-05 04:02:57 +00:00
@util.api ( )
def relearnCards ( self , cards ) :
self . startEditing ( )
scids = anki . utils . ids2str ( cards )
self . collection ( ) . db . execute ( ' update cards set type=3, queue=1 where id in ' + scids )
2021-07-14 00:55:22 +00:00
2021-03-05 04:02:57 +00:00
2023-06-11 21:57:08 +00:00
@util.api ( )
2023-06-15 11:49:02 +00:00
def answerCards ( self , answers ) :
2023-06-11 21:57:08 +00:00
scheduler = self . scheduler ( )
success = [ ]
2023-06-15 11:49:02 +00:00
for answer in answers :
2023-06-11 21:57:08 +00:00
try :
2023-06-15 11:49:02 +00:00
cid = answer [ ' cardId ' ]
ease = answer [ ' ease ' ]
2023-06-11 21:57:08 +00:00
card = self . getCard ( cid )
card . start_timer ( )
2023-06-15 11:49:02 +00:00
scheduler . answerCard ( card , ease )
2023-06-11 21:57:08 +00:00
success . append ( True )
except NotFoundError :
success . append ( False )
return success
2020-07-12 19:53:31 +00:00
@util.api ( )
def cardReviews ( self , deck , startID ) :
2021-01-18 06:13:27 +00:00
return self . database ( ) . all (
' select id, cid, usn, ease, ivl, lastIvl, factor, time, type from revlog ' ' where id>? and cid in (select id from cards where did=?) ' ,
startID ,
self . decks ( ) . id ( deck )
)
2020-07-12 19:53:31 +00:00
2022-09-05 06:29:03 +00:00
@util.api ( )
2022-09-13 23:12:20 +00:00
def getReviewsOfCards ( self , cards ) :
2022-09-20 23:46:26 +00:00
COLUMNS = [ ' id ' , ' usn ' , ' ease ' , ' ivl ' , ' lastIvl ' , ' factor ' , ' time ' , ' type ' ]
QUERY = ' select {} from revlog where cid = ? ' . format ( ' , ' . join ( COLUMNS ) )
result = { }
for card in cards :
query_result = self . database ( ) . all ( QUERY , card )
result [ card ] = [ dict ( zip ( COLUMNS , row ) ) for row in query_result ]
return result
2022-09-13 23:12:20 +00:00
2022-09-05 06:29:03 +00:00
2020-07-12 19:53:31 +00:00
@util.api ( )
def reloadCollection ( self ) :
self . collection ( ) . reset ( )
2021-01-18 06:13:27 +00:00
2020-07-12 19:53:31 +00:00
@util.api ( )
def getLatestReviewID ( self , deck ) :
2021-01-18 06:13:27 +00:00
return self . database ( ) . scalar (
2021-01-18 06:27:20 +00:00
' select max(id) from revlog where cid in (select id from cards where did=?) ' ,
2021-01-18 06:13:27 +00:00
self . decks ( ) . id ( deck )
) or 0
2020-07-12 19:53:31 +00:00
@util.api ( )
def insertReviews ( self , reviews ) :
2021-01-18 06:13:27 +00:00
if len ( reviews ) > 0 :
sql = ' insert into revlog(id,cid,usn,ease,ivl,lastIvl,factor,time,type) values '
for row in reviews :
sql + = ' ( %s ), ' % ' , ' . join ( map ( str , row ) )
sql = sql [ : - 1 ]
self . database ( ) . execute ( sql )
2020-07-12 19:53:31 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-06 19:59:31 +00:00
def notesInfo ( self , notes ) :
2018-01-06 19:48:25 +00:00
result = [ ]
for nid in notes :
try :
2021-03-05 04:06:25 +00:00
note = self . getNote ( nid )
2024-01-21 14:31:21 +00:00
model = note . note_type ( )
2018-01-06 19:48:25 +00:00
fields = { }
for info in model [ ' flds ' ] :
order = info [ ' ord ' ]
name = info [ ' name ' ]
2018-01-09 13:09:38 +00:00
fields [ name ] = { ' value ' : note . fields [ order ] , ' order ' : order }
2018-03-31 21:40:01 +00:00
2018-01-06 19:48:25 +00:00
result . append ( {
' noteId ' : note . id ,
2024-07-02 13:27:19 +00:00
' profile ' : self . window ( ) . pm . name ,
2018-01-06 19:48:25 +00:00
' tags ' : note . tags ,
' fields ' : fields ,
' modelName ' : model [ ' name ' ] ,
2024-06-17 10:03:19 +00:00
' mod ' : note . mod ,
2018-05-07 00:52:24 +00:00
' cards ' : self . collection ( ) . db . list ( ' select id from cards where nid = ? order by ord ' , note . id )
2018-01-06 19:48:25 +00:00
} )
2021-03-05 04:06:25 +00:00
except NotFoundError :
# Anki will give a NotFoundError if the note ID does not exist.
2018-03-11 22:10:07 +00:00
# Best behavior is probably to add an 'empty card' to the
2018-01-06 19:48:25 +00:00
# returned result, so that the items of the input and return
# lists correspond.
result . append ( { } )
2018-05-07 00:52:24 +00:00
2018-01-06 19:48:25 +00:00
return result
2024-06-17 10:03:19 +00:00
@util.api ( )
def notesModTime ( self , notes ) :
result = [ ]
for nid in notes :
try :
note = self . getNote ( nid )
result . append ( {
' noteId ' : note . id ,
' mod ' : note . mod
} )
except NotFoundError :
# Anki will give a NotFoundError if the note ID does not exist.
# Best behavior is probably to add an 'empty card' to the
# returned result, so that the items of the input and return
# lists correspond.
result . append ( { } )
return result
2017-08-03 21:31:47 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2019-02-26 17:28:36 +00:00
def deleteNotes ( self , notes ) :
2024-01-21 14:31:21 +00:00
self . collection ( ) . remove_notes ( notes )
2019-02-26 17:28:36 +00:00
2021-01-18 06:13:27 +00:00
2021-01-05 02:01:56 +00:00
@util.api ( )
def removeEmptyNotes ( self ) :
for model in self . collection ( ) . models . all ( ) :
2024-01-21 14:31:21 +00:00
if self . collection ( ) . models . use_count ( model ) == 0 :
self . collection ( ) . models . remove ( model [ " id " ] )
2021-01-05 02:01:56 +00:00
self . window ( ) . requireReset ( )
2017-08-13 09:05:56 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-09 18:05:00 +00:00
def cardsToNotes ( self , cards ) :
2017-08-12 14:57:28 +00:00
return self . collection ( ) . db . list ( ' select distinct nid from cards where id in ' + anki . utils . ids2str ( cards ) )
2017-08-09 18:05:00 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2024-01-18 21:28:47 +00:00
def guiBrowse ( self , query = None , reorderCards = None ) :
2017-05-28 22:54:28 +00:00
browser = aqt . dialogs . open ( ' Browser ' , self . window ( ) )
2017-05-27 12:14:40 +00:00
browser . activateWindow ( )
2017-05-28 22:54:28 +00:00
2017-07-04 19:35:04 +00:00
if query is not None :
2017-05-27 12:14:40 +00:00
browser . form . searchEdit . lineEdit ( ) . setText ( query )
2017-07-04 19:35:04 +00:00
if hasattr ( browser , ' onSearch ' ) :
browser . onSearch ( )
else :
browser . onSearchActivated ( )
2017-05-28 22:54:28 +00:00
2024-01-18 21:28:47 +00:00
if reorderCards is not None :
if not isinstance ( reorderCards , dict ) :
raise Exception ( ' reorderCards should be a dict: {} ' . format ( reorderCards ) )
if not ( ' columnId ' in reorderCards and ' order ' in reorderCards ) :
raise Exception ( ' Must provide a " columnId " and a " order " property " ' )
cardOrder = reorderCards [ ' order ' ]
if cardOrder not in ( ' ascending ' , ' descending ' ) :
raise Exception ( ' invalid card order: {} ' . format ( reorderCards [ ' order ' ] ) )
cardOrder = Qt . SortOrder . DescendingOrder if cardOrder == ' descending ' else Qt . SortOrder . AscendingOrder
columnId = browser . table . _model . active_column_index ( reorderCards [ ' columnId ' ] )
if columnId == None :
raise Exception ( ' invalid columnId: {} ' . format ( reorderCards [ ' columnId ' ] ) )
browser . table . _on_sort_column_changed ( columnId , cardOrder )
2021-09-15 03:30:36 +00:00
return self . findCards ( query )
2017-05-27 12:14:40 +00:00
2022-03-30 19:26:26 +00:00
@util.api ( )
def guiEditNote ( self , note ) :
Edit . open_dialog_and_show_note_with_id ( note )
2024-01-18 21:28:47 +00:00
@util.api ( )
def guiSelectNote ( self , note ) :
( creator , instance ) = aqt . dialogs . _dialogs [ ' Browser ' ]
if instance is None :
return False
instance . table . clear_selection ( )
instance . table . select_single_card ( note )
return True
2022-03-30 19:26:26 +00:00
2021-12-06 16:59:10 +00:00
@util.api ( )
def guiSelectedNotes ( self ) :
( creator , instance ) = aqt . dialogs . _dialogs [ ' Browser ' ]
if instance is None :
return [ ]
return instance . selectedNotes ( )
2017-05-27 12:14:40 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2019-01-17 22:08:52 +00:00
def guiAddCards ( self , note = None ) :
if note is not None :
collection = self . collection ( )
2024-01-21 14:31:21 +00:00
deck = collection . decks . by_name ( note [ ' deckName ' ] )
2019-01-17 23:40:16 +00:00
if deck is None :
raise Exception ( ' deck was not found: {} ' . format ( note [ ' deckName ' ] ) )
2021-01-18 06:13:27 +00:00
collection . decks . select ( deck [ ' id ' ] )
2019-01-31 23:15:32 +00:00
savedMid = deck . pop ( ' mid ' , None )
2019-01-17 23:40:16 +00:00
2024-01-21 14:31:21 +00:00
model = collection . models . by_name ( note [ ' modelName ' ] )
2019-01-17 22:08:52 +00:00
if model is None :
raise Exception ( ' model was not found: {} ' . format ( note [ ' modelName ' ] ) )
2024-01-21 14:31:21 +00:00
collection . models . set_current ( model )
2021-01-18 06:13:27 +00:00
collection . models . update ( model )
2019-01-17 22:08:52 +00:00
2019-05-04 21:24:13 +00:00
ankiNote = anki . notes . Note ( collection , model )
# fill out card beforehand, so we can be sure of the note id
if ' fields ' in note :
for name , value in note [ ' fields ' ] . items ( ) :
if name in ankiNote :
ankiNote [ name ] = value
2021-06-21 03:35:09 +00:00
self . addMediaFromNote ( ankiNote , note )
2019-05-04 21:24:13 +00:00
if ' tags ' in note :
ankiNote . tags = note [ ' tags ' ]
2019-01-24 06:07:53 +00:00
def openNewWindow ( ) :
2019-05-04 21:24:13 +00:00
nonlocal ankiNote
2019-01-24 06:07:53 +00:00
addCards = aqt . dialogs . open ( ' AddCards ' , self . window ( ) )
2019-01-31 23:15:32 +00:00
if savedMid :
deck [ ' mid ' ] = savedMid
2022-04-12 18:50:36 +00:00
addCards . editor . set_note ( ankiNote )
2019-01-24 06:07:53 +00:00
addCards . activateWindow ( )
2019-01-24 07:36:11 +00:00
aqt . dialogs . open ( ' AddCards ' , self . window ( ) )
2019-05-04 21:24:13 +00:00
addCards . setAndFocusNote ( addCards . editor . note )
currentWindow = aqt . dialogs . _dialogs [ ' AddCards ' ] [ 1 ]
2019-01-24 06:07:53 +00:00
if currentWindow is not None :
2020-01-05 19:42:59 +00:00
currentWindow . closeWithCallback ( openNewWindow )
2019-01-24 06:07:53 +00:00
else :
openNewWindow ( )
2019-05-04 21:24:13 +00:00
return ankiNote . id
2019-05-04 21:24:13 +00:00
2019-01-24 06:07:53 +00:00
else :
addCards = aqt . dialogs . open ( ' AddCards ' , self . window ( ) )
addCards . activateWindow ( )
2017-05-27 12:14:40 +00:00
2019-05-04 21:24:13 +00:00
return addCards . editor . note . id
2021-01-18 06:13:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-07-01 19:41:27 +00:00
def guiReviewActive ( self ) :
2017-07-03 00:27:31 +00:00
return self . reviewer ( ) . card is not None and self . window ( ) . state == ' review '
2017-07-01 19:41:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-07-01 19:41:27 +00:00
def guiCurrentCard ( self ) :
2017-07-01 19:49:44 +00:00
if not self . guiReviewActive ( ) :
2018-03-11 22:10:07 +00:00
raise Exception ( ' Gui review is not currently active. ' )
2017-07-01 19:41:27 +00:00
reviewer = self . reviewer ( )
card = reviewer . card
2024-01-21 14:31:21 +00:00
model = card . note_type ( )
2017-07-03 00:52:57 +00:00
note = card . note ( )
fields = { }
for info in model [ ' flds ' ] :
2017-07-05 20:01:13 +00:00
order = info [ ' ord ' ]
name = info [ ' name ' ]
fields [ name ] = { ' value ' : note . fields [ order ] , ' order ' : order }
2021-01-18 06:13:27 +00:00
buttonList = reviewer . _answerButtonList ( )
return {
' cardId ' : card . id ,
' fields ' : fields ,
' fieldOrder ' : card . ord ,
' question ' : util . cardQuestion ( card ) ,
' answer ' : util . cardAnswer ( card ) ,
' buttons ' : [ b [ 0 ] for b in buttonList ] ,
' nextReviews ' : [ reviewer . mw . col . sched . nextIvlStr ( reviewer . card , b [ 0 ] , True ) for b in buttonList ] ,
' modelName ' : model [ ' name ' ] ,
' deckName ' : self . deckNameFromId ( card . did ) ,
' css ' : model [ ' css ' ] ,
' template ' : card . template ( ) [ ' name ' ]
}
2017-06-29 04:17:11 +00:00
2017-06-02 12:19:33 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-16 12:04:05 +00:00
def guiStartCardTimer ( self ) :
if not self . guiReviewActive ( ) :
return False
card = self . reviewer ( ) . card
if card is not None :
card . startTimer ( )
return True
2021-01-18 06:13:27 +00:00
return False
2017-08-16 12:04:05 +00:00
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-06-16 16:17:53 +00:00
def guiShowQuestion ( self ) :
2017-07-01 19:41:27 +00:00
if self . guiReviewActive ( ) :
self . reviewer ( ) . _showQuestion ( )
return True
2021-01-18 06:13:27 +00:00
return False
2017-06-02 12:19:33 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-06-16 16:17:53 +00:00
def guiShowAnswer ( self ) :
2017-07-01 19:41:27 +00:00
if self . guiReviewActive ( ) :
2017-06-02 12:19:33 +00:00
self . window ( ) . reviewer . _showAnswer ( )
2017-07-01 19:41:27 +00:00
return True
2021-01-18 06:13:27 +00:00
return False
2017-06-02 12:19:33 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-07-01 20:16:51 +00:00
def guiAnswerCard ( self , ease ) :
2017-07-01 19:41:27 +00:00
if not self . guiReviewActive ( ) :
2017-06-29 04:17:11 +00:00
return False
2017-07-01 19:41:27 +00:00
reviewer = self . reviewer ( )
if reviewer . state != ' answer ' :
2017-06-29 04:17:11 +00:00
return False
2017-07-01 20:16:51 +00:00
if ease < = 0 or ease > self . scheduler ( ) . answerButtons ( reviewer . card ) :
2017-06-29 04:17:11 +00:00
return False
2017-06-02 12:19:33 +00:00
2017-07-01 19:41:27 +00:00
reviewer . _answerCard ( ease )
return True
2017-06-02 12:19:33 +00:00
2017-07-03 00:27:31 +00:00
2023-05-18 09:30:14 +00:00
@util.api ( )
def guiUndo ( self ) :
self . window ( ) . undo ( )
return True
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-07-02 17:35:14 +00:00
def guiDeckOverview ( self , name ) :
collection = self . collection ( )
if collection is not None :
2024-01-21 14:31:21 +00:00
deck = collection . decks . by_name ( name )
2017-07-02 20:38:08 +00:00
if deck is not None :
2017-07-02 17:35:14 +00:00
collection . decks . select ( deck [ ' id ' ] )
self . window ( ) . onOverview ( )
return True
2017-07-03 00:27:31 +00:00
2017-07-02 17:35:14 +00:00
return False
2017-07-03 00:27:31 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-07-02 17:35:14 +00:00
def guiDeckBrowser ( self ) :
2017-07-03 00:27:31 +00:00
self . window ( ) . moveToState ( ' deckBrowser ' )
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-07-03 00:27:31 +00:00
def guiDeckReview ( self , name ) :
if self . guiDeckOverview ( name ) :
self . window ( ) . moveToState ( ' review ' )
return True
2021-01-18 06:13:27 +00:00
return False
2017-07-03 00:27:31 +00:00
2018-03-31 21:40:01 +00:00
2023-06-17 09:45:39 +00:00
@util.api ( )
def guiImportFile ( self , path = None ) :
"""
Open Import File ( Ctrl + Shift + I ) dialog with provided file path .
If no path is given , the user will be prompted to select a file .
2023-07-06 17:03:46 +00:00
Only supported from Anki version > = 2.1 .52
2023-06-17 09:45:39 +00:00
path : string
import file path , note on Windows you must use forward slashes .
"""
2023-07-06 17:03:46 +00:00
if anki_version > = ( 2 , 1 , 52 ) :
from aqt . import_export . importing import import_file , prompt_for_file_then_import
else :
raise Exception ( ' guiImportFile is only supported from Anki version >=2.1.52 ' )
if hasattr ( Qt , ' WindowStaysOnTopHint ' ) :
# Qt5
WindowOnTopFlag = Qt . WindowStaysOnTopHint
elif hasattr ( Qt , ' WindowType ' ) and hasattr ( Qt . WindowType , ' WindowStaysOnTopHint ' ) :
# Qt6
WindowOnTopFlag = Qt . WindowType . WindowStaysOnTopHint
else :
# Unsupported, don't try to bring window to top
WindowOnTopFlag = None
2023-06-17 09:45:39 +00:00
# Bring window to top for user to review import settings.
2023-07-06 17:03:46 +00:00
if WindowOnTopFlag is not None :
try :
# [Step 1/2] set always on top flag, show window (it stays on top for now)
self . window ( ) . setWindowFlags ( self . window ( ) . windowFlags ( ) | WindowOnTopFlag )
self . window ( ) . show ( )
finally :
# [Step 2/2] clear always on top flag, show window (it doesn't stay on top anymore)
self . window ( ) . setWindowFlags ( self . window ( ) . windowFlags ( ) & ~ WindowOnTopFlag )
self . window ( ) . show ( )
2023-06-17 09:45:39 +00:00
if path is None :
prompt_for_file_then_import ( self . window ( ) )
else :
import_file ( self . window ( ) , path )
2020-01-05 23:42:08 +00:00
@util.api ( )
2017-08-29 02:24:08 +00:00
def guiExitAnki ( self ) :
timer = QTimer ( )
2020-01-05 23:42:08 +00:00
timer . timeout . connect ( self . window ( ) . close )
2017-08-29 02:24:08 +00:00
timer . start ( 1000 ) # 1s should be enough to allow the response to be sent.
2017-06-02 12:19:33 +00:00
2018-03-31 21:40:01 +00:00
2021-07-14 00:55:22 +00:00
@util.api ( )
def guiCheckDatabase ( self ) :
self . window ( ) . onCheckDB ( )
return True
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-06 19:59:31 +00:00
def addNotes ( self , notes ) :
results = [ ]
2024-06-18 15:34:27 +00:00
errs = [ ]
2018-05-06 19:59:31 +00:00
for note in notes :
try :
2018-05-06 20:24:40 +00:00
results . append ( self . addNote ( note ) )
2024-06-18 15:34:27 +00:00
except Exception as e :
# I specifically chose to continue, so we gather all the errors of all notes (ie not break)
errs . append ( str ( e ) )
if errs :
# Roll back the changes so on error nothing happens
self . deleteNotes ( results )
raise Exception ( str ( errs ) )
2018-03-31 21:40:01 +00:00
2018-05-06 19:59:31 +00:00
return results
2018-01-06 19:48:25 +00:00
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api ( )
2018-05-06 19:59:31 +00:00
def canAddNotes ( self , notes ) :
results = [ ]
for note in notes :
2018-05-06 20:24:40 +00:00
results . append ( self . canAddNote ( note ) )
2018-03-31 21:40:01 +00:00
2018-05-06 19:59:31 +00:00
return results
2018-02-21 13:16:52 +00:00
2024-02-23 05:39:35 +00:00
@util.api ( )
def canAddNotesWithErrorDetail ( self , notes ) :
results = [ ]
for note in notes :
results . append ( self . canAddNoteWithErrorDetail ( note ) )
return results
2018-02-21 13:16:52 +00:00
2020-03-17 05:29:49 +00:00
@util.api ( )
def exportPackage ( self , deck , path , includeSched = False ) :
collection = self . collection ( )
if collection is not None :
2024-01-21 14:31:21 +00:00
deck = collection . decks . by_name ( deck )
2020-03-17 05:29:49 +00:00
if deck is not None :
exporter = AnkiPackageExporter ( collection )
2020-04-22 01:22:10 +00:00
exporter . did = deck [ ' id ' ]
2020-03-17 05:29:49 +00:00
exporter . includeSched = includeSched
exporter . exportInto ( path )
return True
2021-01-18 06:13:27 +00:00
2020-03-17 05:29:49 +00:00
return False
2021-01-18 06:13:27 +00:00
2020-05-01 16:19:47 +00:00
@util.api ( )
def importPackage ( self , path ) :
collection = self . collection ( )
if collection is not None :
try :
self . startEditing ( )
importer = AnkiPackageImporter ( collection , path )
importer . run ( )
except :
raise
else :
return True
return False
2020-03-17 05:29:49 +00:00
2022-05-20 21:00:38 +00:00
@util.api ( )
def apiReflect ( self , scopes = None , actions = None ) :
if not isinstance ( scopes , list ) :
raise Exception ( ' scopes has invalid value ' )
if not ( actions is None or isinstance ( actions , list ) ) :
raise Exception ( ' actions has invalid value ' )
cls = type ( self )
scopes2 = [ ]
result = { ' scopes ' : scopes2 }
if ' actions ' in scopes :
if actions is None :
actions = dir ( cls )
methodNames = [ ]
for methodName in actions :
if not isinstance ( methodName , str ) :
pass
method = getattr ( cls , methodName , None )
if method is not None and getattr ( method , ' api ' , False ) :
methodNames . append ( methodName )
scopes2 . append ( ' actions ' )
result [ ' actions ' ] = methodNames
return result
2016-05-21 22:10:12 +00:00
#
2018-05-07 00:52:24 +00:00
# Entry
2016-05-21 22:10:12 +00:00
#
2022-03-30 18:43:31 +00:00
# when run inside Anki, `__name__` would be either numeric,
# or, if installed via `link.sh`, `AnkiConnectDev`
if __name__ != " plugin " :
2022-04-20 17:30:20 +00:00
if platform . system ( ) == " Windows " and anki_version == ( 2 , 1 , 50 ) :
util . patch_anki_2_1_50_having_null_stdout_on_windows ( )
2022-04-10 23:10:27 +00:00
Edit . register_with_anki ( )
2022-03-30 19:26:26 +00:00
2022-03-30 18:43:31 +00:00
ac = AnkiConnect ( )
ac . initLogging ( )
ac . startWebServer ( )