├── README.md └── anki-quizlet.py /README.md: -------------------------------------------------------------------------------- 1 | Anki-Quizlet 2 | ============ 3 | 4 | Anki addon that loads decks from Quizlet.com -------------------------------------------------------------------------------- /anki-quizlet.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: Quizlet plugin for Anki 2.0 3 | # Purpose: Import decks from Quizlet.com into Anki 2.0 4 | # 5 | # Author: Rolph Recto 6 | # 7 | # Created: 12/06/2012 8 | # Copyright: (c) Rolph Recto 2012 9 | # Licence: 10 | #------------------------------------------------------------------------------- 11 | #!/usr/bin/env python 12 | 13 | __window = None 14 | 15 | import sys 16 | import math 17 | import time 18 | import datetime as dt 19 | import urllib as url 20 | import urllib2 as url2 21 | import json 22 | 23 | #Anki 24 | from aqt import mw 25 | from aqt.qt import * 26 | 27 | #PyQT 28 | from PyQt4.QtGui import * 29 | from PyQt4.Qt import Qt 30 | 31 | #copied straight from anki.stdmodels 32 | #it is necessary to create a custom model 33 | #because the user might have changed the default model 34 | def addCustomModel(name, col): 35 | """create a new custom model for the imported deck""" 36 | mm = col.models 37 | m = mm.new(_("Basic")+" ({0})".format(name)) 38 | fm = mm.newField(_("Front")) 39 | mm.addField(m, fm) 40 | fm = mm.newField(_("Back")) 41 | mm.addField(m, fm) 42 | t = mm.newTemplate(_("Card 1")) 43 | t['qfmt'] = _("{{Front}}") 44 | t['afmt'] = "{{FrontSide}}\n\n
\n\n"+_("{{Back}}") 45 | mm.addTemplate(m, t) 46 | mm.add(m) 47 | return m 48 | 49 | class QuizletWindow(QWidget): 50 | """main window of Quizlet plugin; shows search results""" 51 | 52 | PAGE_FIRST = 1 53 | PAGE_PREVIOUS = 2 54 | PAGE_NEXT = 3 55 | PAGE_LAST = 4 56 | RESULT_ERROR = -1 57 | RESULTS_PER_PAGE = 50 58 | __APIKEY = "ke9tZw8YM6" #used to access Quizlet API 59 | 60 | def __init__(self): 61 | super(QuizletWindow, self).__init__() 62 | 63 | self.results = None 64 | self.thread = None 65 | self.name = "" 66 | self.user = "" 67 | self.sort = "most_studied" 68 | self.result_page = -1 69 | 70 | self.initGUI() 71 | 72 | def initGUI(self): 73 | """create the GUI skeleton""" 74 | 75 | self.box_top = QVBoxLayout() 76 | self.box_upper = QHBoxLayout() 77 | 78 | #left side 79 | self.box_left = QVBoxLayout() 80 | 81 | #name field 82 | self.box_name = QHBoxLayout() 83 | self.label_name = QLabel("Name") 84 | self.text_name = QLineEdit("",self) 85 | 86 | self.box_name.addWidget(self.label_name) 87 | self.box_name.addWidget(self.text_name) 88 | 89 | #user field 90 | self.box_user = QHBoxLayout() 91 | self.label_user = QLabel("User") 92 | self.text_user = QLineEdit("",self) 93 | 94 | self.box_user.addWidget(self.label_user) 95 | self.box_user.addWidget(self.text_user) 96 | 97 | #add layouts to left 98 | self.box_left.addLayout(self.box_name) 99 | self.box_left.addLayout(self.box_user) 100 | 101 | #right side 102 | self.box_right = QVBoxLayout() 103 | 104 | #sort type 105 | self.box_sort = QHBoxLayout() 106 | self.label_sort = QLabel("Sort by:", self) 107 | self.buttongroup_sort = QButtonGroup() 108 | self.radio_popularity = QRadioButton("Popularity", self) 109 | self.radio_name = QRadioButton("Name", self) 110 | self.radio_date = QRadioButton("Date created", self) 111 | self.radio_popularity.setChecked(True) 112 | self.buttongroup_sort.addButton(self.radio_popularity) 113 | self.buttongroup_sort.addButton(self.radio_name) 114 | self.buttongroup_sort.addButton(self.radio_date) 115 | 116 | self.box_sort.addWidget(self.label_sort) 117 | self.box_sort.addWidget(self.radio_popularity) 118 | self.box_sort.addWidget(self.radio_name) 119 | self.box_sort.addWidget(self.radio_date) 120 | self.box_sort.addStretch(1) 121 | 122 | #search button 123 | self.box_search = QHBoxLayout() 124 | self.button_search = QPushButton("Search", self) 125 | 126 | self.box_search.addStretch(1) 127 | self.box_search.addWidget(self.button_search) 128 | 129 | self.button_search.clicked.connect(self.onSearch) 130 | 131 | #add layouts to right 132 | self.box_right.addLayout(self.box_sort) 133 | self.box_right.addLayout(self.box_search) 134 | 135 | #add left and right layouts to upper 136 | self.box_upper.addLayout(self.box_left) 137 | self.box_upper.addSpacing(20) 138 | self.box_upper.addLayout(self.box_right) 139 | 140 | #table navigation 141 | self.box_tablenav = QHBoxLayout() 142 | 143 | self.button_first = QPushButton("<<", self) 144 | self.button_first.setMaximumWidth(30) 145 | self.button_first.setVisible(False) 146 | 147 | self.button_previous = QPushButton("<", self) 148 | self.button_previous.setMaximumWidth(30) 149 | self.button_previous.setVisible(False) 150 | 151 | self.button_current = QPushButton(str(self.result_page), self) 152 | self.button_current.setMaximumWidth(50) 153 | self.button_current.setVisible(False) 154 | 155 | self.button_next = QPushButton(">", self) 156 | self.button_next.setMaximumWidth(30) 157 | self.button_next.setVisible(False) 158 | 159 | self.button_last = QPushButton(">>", self) 160 | self.button_last.setMaximumWidth(30) 161 | self.button_last.setVisible(False) 162 | 163 | self.box_tablenav.addStretch(1) 164 | self.box_tablenav.addWidget(self.button_first) 165 | self.box_tablenav.addWidget(self.button_previous) 166 | self.box_tablenav.addWidget(self.button_current) 167 | self.box_tablenav.addWidget(self.button_next) 168 | self.box_tablenav.addWidget(self.button_last) 169 | self.box_tablenav.addStretch(1) 170 | 171 | self.button_first.clicked.connect(self.onPageFirst) 172 | self.button_previous.clicked.connect(self.onPagePrevious) 173 | self.button_current.clicked.connect(self.onPageCurrent) 174 | self.button_next.clicked.connect(self.onPageNext) 175 | self.button_last.clicked.connect(self.onPageLast) 176 | 177 | #results label 178 | self.label_results = QLabel("") 179 | 180 | #table of results 181 | self.table_results = QTableWidget(2, 4, self) 182 | self.table_results.setHorizontalHeaderLabels(["Name", "User", 183 | "Items", "Date created"]) 184 | self.table_results.verticalHeader().hide() 185 | self.table_results.setSelectionBehavior(QAbstractItemView.SelectRows) 186 | self.table_results.setSelectionMode(QAbstractItemView.SingleSelection) 187 | self.table_results.setEditTriggers(QAbstractItemView.NoEditTriggers) 188 | self.table_results.horizontalHeader().setSortIndicatorShown(False) 189 | self.table_results.horizontalHeader().setClickable(False) 190 | self.table_results.horizontalHeader().setResizeMode(QHeaderView.Interactive) 191 | self.table_results.horizontalHeader().setStretchLastSection(True) 192 | self.table_results.horizontalHeader().setMinimumSectionSize(100) 193 | self.table_results.verticalHeader().setResizeMode(QHeaderView.Fixed) 194 | self.table_results.setMinimumHeight(275) 195 | self.table_results.setVisible(False) 196 | 197 | #import selected deck 198 | self.box_import = QHBoxLayout() 199 | self.button_import = QPushButton("Import Deck", self) 200 | self.button_import.setVisible(False) 201 | 202 | self.box_import.addStretch(1) 203 | self.box_import.addWidget(self.button_import) 204 | 205 | self.button_import.clicked.connect(self.onImportDeck) 206 | 207 | #add all widgets to top layout 208 | self.box_top.addLayout(self.box_upper) 209 | self.box_top.addLayout(self.box_tablenav) 210 | self.box_top.addWidget(self.label_results) 211 | self.box_top.addWidget(self.table_results) 212 | self.box_top.addLayout(self.box_import) 213 | self.box_top.addStretch(1) 214 | self.setLayout(self.box_top) 215 | 216 | self.setMinimumWidth(500) 217 | self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) 218 | self.setWindowTitle("Import from Quizlet") 219 | self.show() 220 | 221 | def onSearch(self): 222 | """user clicked search button; load first page of results""" 223 | self.name = self.text_name.text() 224 | self.user = self.text_user.text() 225 | self.result_page = -1 226 | 227 | #sort 228 | if self.buttongroup_sort.checkedButton() == self.radio_popularity: 229 | self.sort = "most_studied" 230 | elif self.buttongroup_sort.checkedButton() == self.radio_name: 231 | self.sort = "title" 232 | elif self.buttongroup_sort.checkedButton() == self.radio_date: 233 | self.sort = "most_recent" 234 | 235 | self.fetchResults() 236 | 237 | def onImportDeck(self): 238 | """user clicked Import Deck button, load the deck from Quizlet""" 239 | #find the selected deck in the table 240 | index = self.table_results.currentRow() 241 | 242 | #set the GUI 243 | self.hideTable() 244 | self.button_search.setEnabled(False) 245 | self.label_results.setText( ("Importing deck {0} ..." 246 | .format(self.results["sets"][index]["title"])) ) 247 | 248 | #build URL 249 | deck_url = ("https://api.quizlet.com/2.0/sets/{0}/terms". 250 | format(self.results["sets"][index]["id"])) 251 | 252 | deck_url += "?client_id={0}".format(QuizletWindow.__APIKEY) 253 | 254 | #stop the previous thread first 255 | if not self.thread == None: 256 | self.thread.terminate() 257 | 258 | #download the data! 259 | self.thread = QuizletDownloader(self, deck_url) 260 | self.thread.start() 261 | 262 | while not self.thread.isFinished(): 263 | mw.app.processEvents() 264 | self.thread.wait(50) 265 | 266 | #error with fetching data 267 | if self.thread.error: 268 | self.label_results.setText( ("Failed to load deck {0}!" 269 | .format(self.results["sets"][index]["title"])) ) 270 | #everything went through! 271 | else: 272 | terms = self.thread.results 273 | self.createDeck(self.results["sets"][index]["title"], terms) 274 | 275 | self.showTable() 276 | self.button_search.setEnabled(True) 277 | self.label_results.setText( ("Successfully imported deck {0}." 278 | .format(self.results["sets"][index]["title"])) ) 279 | 280 | self.thread.terminate() 281 | self.thread = None 282 | 283 | def createDeck(self, name, terms): 284 | """create new Anki deck from downloaded data""" 285 | #create new deck and custom model 286 | deck = mw.col.decks.get(mw.col.decks.id(name)) 287 | model = addCustomModel(name, mw.col) 288 | 289 | #assign custom model to new deck 290 | mw.col.decks.select(deck["id"]) 291 | mw.col.decks.get(deck)["mid"] = model["id"] 292 | mw.col.decks.save(deck) 293 | 294 | #assign new deck to custom model 295 | mw.col.models.setCurrent(model) 296 | mw.col.models.current()["did"] = deck["id"] 297 | mw.col.models.save(model) 298 | 299 | for term in terms: 300 | note = mw.col.newNote() 301 | note["Front"] = term["term"] 302 | note["Back"] = term["definition"] 303 | mw.col.addNote(note) 304 | 305 | mw.col.reset() 306 | mw.reset() 307 | 308 | def onPageFirst(self): 309 | """first page button clicked""" 310 | self.__changePage(QuizletWindow.PAGE_FIRST) 311 | 312 | def onPagePrevious(self): 313 | """first page button clicked""" 314 | self.__changePage(QuizletWindow.PAGE_PREVIOUS) 315 | 316 | def onPageCurrent(self): 317 | """let user jump to any page""" 318 | page, ok = QInputDialog.getInteger(self, "Jump to Page", 319 | "What page? ({0} - {1})".format(1, self.results["total_pages"]), 320 | 1, 1, self.results["total_pages"]) 321 | 322 | if ok: 323 | self.fetchResults(page) 324 | 325 | def onPageNext(self): 326 | """first page button clicked""" 327 | self.__changePage(QuizletWindow.PAGE_NEXT) 328 | 329 | def onPageLast(self): 330 | """first page button clicked""" 331 | self.__changePage(QuizletWindow.PAGE_LAST) 332 | 333 | def __changePage(self, change): 334 | """determine what page to fetch""" 335 | if change == QuizletWindow.PAGE_FIRST: 336 | self.fetchResults(1) 337 | elif change == QuizletWindow.PAGE_PREVIOUS: 338 | if self.result_page - 1 >= 1: 339 | self.fetchResults(self.result_page-1) 340 | elif change == QuizletWindow.PAGE_NEXT: 341 | if self.result_page + 1 <= self.results["total_pages"]: 342 | self.fetchResults(self.result_page+1) 343 | elif change == QuizletWindow.PAGE_LAST: 344 | self.fetchResults( self.results["total_pages"] ) 345 | 346 | def showTable(self, show=True): 347 | """set results table visible/invisible""" 348 | self.button_first.setVisible(show) 349 | self.button_previous.setVisible(show) 350 | self.button_current.setVisible(show) 351 | self.button_next.setVisible(show) 352 | self.button_last.setVisible(show) 353 | self.table_results.setVisible(show) 354 | self.button_import.setVisible(show) 355 | 356 | def hideTable(self): 357 | """make results table invisible""" 358 | self.showTable(False) 359 | 360 | def loadResultsToTable(self): 361 | """insert data from results dict into table widget""" 362 | #clear table first 363 | self.table_results.setRowCount(0) 364 | deckList = self.results["sets"] 365 | 366 | #iterate through the decks and add them to the table 367 | for index in range(len(deckList)): 368 | if index+1 > self.table_results.rowCount(): 369 | self.table_results.insertRow(index) 370 | 371 | #deck name 372 | name = QTableWidgetItem(deckList[index]["title"]) 373 | name.setToolTip(deckList[index]["title"]) 374 | self.table_results.setItem(index, 0, name) 375 | 376 | #user who created deck 377 | user = QTableWidgetItem(deckList[index]["created_by"]) 378 | user.setToolTip(deckList[index]["created_by"]) 379 | self.table_results.setItem(index, 1, user) 380 | 381 | #number of items in the deck 382 | items = QTableWidgetItem(str(deckList[index]["term_count"])) 383 | items.setToolTip(str(deckList[index]["term_count"])) 384 | self.table_results.setItem(index, 2, items) 385 | 386 | #last date that the deck was modified 387 | date_str = time.strftime("%m/%d/%Y", 388 | time.localtime(deckList[index]["created_date"])) 389 | date = QTableWidgetItem(date_str) 390 | date.setToolTip(date_str) 391 | self.table_results.setItem(index, 3, date) 392 | 393 | def getResultsDescription(self): 394 | """return a description of search parameters""" 395 | #if textfields are empty, return an error 396 | if self.name == "" and self.user == "": 397 | return "Error: Must have input to search!" 398 | #search for deck name only 399 | elif not self.name == "" and self.user == "": 400 | return "Searching for \"{0}\" ...".format(self.name) 401 | #search for deck name and user 402 | elif not self.name == "" and not self.user == "": 403 | return ("Searching for \"{0}\" by user {1} ..." 404 | .format(self.name, self.user)) 405 | #search for user only 406 | elif self.name == "" and not self.user == "": 407 | return "Searching for decks by user {0} ...".format(self.user) 408 | 409 | def fetchResults(self, page=1): 410 | """load results""" 411 | 412 | #if the page being fetched is the same as the current page, 413 | #don't fetch it! 414 | if page == self.result_page: return 415 | 416 | global __APIKEY 417 | 418 | self.results = None 419 | self.hideTable() 420 | self.label_results.setText(self.getResultsDescription()) 421 | 422 | #textfields are empty 423 | if self.label_results.text() == "Error: Must have input to search!": 424 | return 425 | 426 | #build search URL 427 | search_url = "https://api.quizlet.com/2.0/search/sets" 428 | search_url += "?q={0}".format(self.name) 429 | if not self.user == "": 430 | search_url += "&creator={0}".format(self.user) 431 | search_url += "&page={0}".format(page) 432 | search_url += "&per_page={0}".format(QuizletWindow.RESULTS_PER_PAGE) 433 | search_url += "&sort={0}".format(self.sort) 434 | search_url += "&client_id={0}".format(QuizletWindow.__APIKEY) 435 | 436 | #stop the previous thread first 437 | if not self.thread == None: 438 | self.thread.terminate() 439 | 440 | #download the data! 441 | self.thread = QuizletDownloader(self, search_url) 442 | self.thread.start() 443 | 444 | while not self.thread.isFinished(): 445 | mw.app.processEvents() 446 | self.thread.wait(50) 447 | 448 | self.results = self.thread.results 449 | 450 | #error with fetching data; don't display table 451 | if self.thread.error: 452 | self.setPage(QuizletWindow.RESULT_ERROR) 453 | #everything went through! 454 | else: 455 | self.setPage(page) 456 | self.loadResultsToTable() 457 | self.showTable() 458 | 459 | self.thread.terminate() 460 | self.thread = None 461 | 462 | def setPage(self, page): 463 | """set page of results to load""" 464 | if page == QuizletWindow.RESULT_ERROR: 465 | self.result_page = -1 466 | self.button_current.setText(" ") 467 | self.label_results.setText( ("No results found!") ) 468 | else: 469 | num_results = self.results["total_results"] 470 | first = ((page-1)*50)+1 471 | last = (page*QuizletWindow.RESULTS_PER_PAGE 472 | if page*QuizletWindow.RESULTS_PER_PAGE < num_results 473 | else num_results) 474 | self.result_page = page 475 | self.button_current.setText(str(page)) 476 | self.label_results.setText( ("Displaying results {0} - {1} of {2}." 477 | .format(first, last, num_results)) ) 478 | self.table_results.verticalHeader().setOffset(first) 479 | 480 | 481 | class QuizletDownloader(QThread): 482 | """thread that downloads results from the Quizlet API""" 483 | 484 | def __init__(self, window, url): 485 | super(QuizletDownloader, self).__init__() 486 | self.window=window 487 | self.url = url 488 | self.error = False 489 | self.results = None 490 | 491 | def run(self): 492 | """run thread; download results!""" 493 | try: 494 | self.results = json.load(url2.urlopen(self.url)) 495 | except url2.URLError: 496 | self.error = True 497 | else: 498 | #if no results, there was an error 499 | if self.results == None: 500 | self.error = True 501 | 502 | 503 | def runQuizletPlugin(): 504 | """menu item pressed; display search window""" 505 | global __window 506 | __window = QuizletWindow() 507 | 508 | #create menu item 509 | action = QAction("Import from Quizlet", mw) 510 | mw.connect(action, SIGNAL("triggered()"), runQuizletPlugin) 511 | mw.form.menuTools.addAction(action) --------------------------------------------------------------------------------