2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
2019-12-20 10:19:03 +01:00
|
|
|
import json
|
2013-10-20 03:57:42 +02:00
|
|
|
import os
|
|
|
|
import re
|
2019-12-20 10:19:03 +01:00
|
|
|
import shutil
|
2013-10-20 03:57:42 +02:00
|
|
|
import traceback
|
2017-09-30 11:29:21 +02:00
|
|
|
import unicodedata
|
2019-12-20 10:19:03 +01:00
|
|
|
import zipfile
|
2020-03-06 05:36:05 +01:00
|
|
|
from concurrent.futures import Future
|
2021-02-02 15:00:29 +01:00
|
|
|
from typing import Any, Dict, Optional
|
2013-10-20 03:57:42 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
import anki.importing as importing
|
2019-12-20 10:19:03 +01:00
|
|
|
import aqt.deckchooser
|
2013-10-20 03:57:42 +02:00
|
|
|
import aqt.forms
|
|
|
|
import aqt.modelchooser
|
2021-02-01 13:08:56 +01:00
|
|
|
from anki.importing.apkg import AnkiPackageImporter
|
2020-01-15 04:49:26 +01:00
|
|
|
from aqt import AnkiQt, gui_hooks
|
2019-12-20 10:19:03 +01:00
|
|
|
from aqt.qt import *
|
2019-12-23 01:34:10 +01:00
|
|
|
from aqt.utils import (
|
2021-01-25 14:45:47 +01:00
|
|
|
HelpPage,
|
2019-12-23 01:34:10 +01:00
|
|
|
askUser,
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button,
|
2019-12-23 01:34:10 +01:00
|
|
|
getFile,
|
2021-03-04 19:55:35 +01:00
|
|
|
getText,
|
2019-12-23 01:34:10 +01:00
|
|
|
openHelp,
|
|
|
|
showInfo,
|
|
|
|
showText,
|
|
|
|
showWarning,
|
|
|
|
tooltip,
|
2020-02-27 11:32:57 +01:00
|
|
|
tr,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2019-12-20 10:19:03 +01:00
|
|
|
|
2013-10-20 03:57:42 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
class ChangeMap(QDialog):
|
2021-02-02 15:00:29 +01:00
|
|
|
def __init__(self, mw: AnkiQt, model: Dict, current: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
QDialog.__init__(self, mw, Qt.Window)
|
|
|
|
self.mw = mw
|
|
|
|
self.model = model
|
|
|
|
self.frm = aqt.forms.changemap.Ui_ChangeMap()
|
|
|
|
self.frm.setupUi(self)
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
n = 0
|
|
|
|
setCurrent = False
|
2019-12-23 01:34:10 +01:00
|
|
|
for field in self.model["flds"]:
|
2021-03-26 05:21:04 +01:00
|
|
|
item = QListWidgetItem(tr.importing_map_to(val=field["name"]))
|
2012-12-21 08:51:59 +01:00
|
|
|
self.frm.fields.addItem(item)
|
2019-12-23 01:34:10 +01:00
|
|
|
if current == field["name"]:
|
2012-12-21 08:51:59 +01:00
|
|
|
setCurrent = True
|
|
|
|
self.frm.fields.setCurrentRow(n)
|
|
|
|
n += 1
|
2021-03-26 04:48:26 +01:00
|
|
|
self.frm.fields.addItem(QListWidgetItem(tr.importing_map_to_tags()))
|
|
|
|
self.frm.fields.addItem(QListWidgetItem(tr.importing_ignore_field()))
|
2012-12-21 08:51:59 +01:00
|
|
|
if not setCurrent:
|
|
|
|
if current == "_tags":
|
|
|
|
self.frm.fields.setCurrentRow(n)
|
|
|
|
else:
|
2019-12-23 01:34:10 +01:00
|
|
|
self.frm.fields.setCurrentRow(n + 1)
|
2020-07-24 20:38:34 +02:00
|
|
|
self.field: Optional[str] = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def getField(self) -> str:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.exec_()
|
|
|
|
return self.field
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def accept(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
row = self.frm.fields.currentRow()
|
2019-12-23 01:34:10 +01:00
|
|
|
if row < len(self.model["flds"]):
|
|
|
|
self.field = self.model["flds"][row]["name"]
|
2012-12-21 08:51:59 +01:00
|
|
|
elif row == self.frm.fields.count() - 2:
|
|
|
|
self.field = "_tags"
|
|
|
|
else:
|
|
|
|
self.field = None
|
|
|
|
QDialog.accept(self)
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def reject(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.accept()
|
|
|
|
|
|
|
|
|
2020-03-06 05:36:05 +01:00
|
|
|
# called by importFile() when importing a mappable file like .csv
|
2021-02-02 15:00:29 +01:00
|
|
|
# ImportType = Union[Importer,AnkiPackageImporter, TextImporter]
|
|
|
|
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
class ImportDialog(QDialog):
|
2021-03-04 19:39:43 +01:00
|
|
|
_DEFAULT_FILE_DELIMITER = "\t"
|
|
|
|
|
2021-02-02 15:00:29 +01:00
|
|
|
def __init__(self, mw: AnkiQt, importer: Any) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
QDialog.__init__(self, mw, Qt.Window)
|
|
|
|
self.mw = mw
|
|
|
|
self.importer = importer
|
|
|
|
self.frm = aqt.forms.importing.Ui_ImportDialog()
|
|
|
|
self.frm.setupUi(self)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(
|
|
|
|
self.frm.buttonBox.button(QDialogButtonBox.Help).clicked, self.helpRequested
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2021-01-07 05:24:49 +01:00
|
|
|
disable_help_button(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setupMappingFrame()
|
|
|
|
self.setupOptions()
|
|
|
|
self.modelChanged()
|
2013-04-15 06:46:07 +02:00
|
|
|
self.frm.autoDetect.setVisible(self.importer.needDelimiter)
|
2020-01-15 07:53:24 +01:00
|
|
|
gui_hooks.current_note_type_did_change.append(self.modelChanged)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self.frm.autoDetect.clicked, self.onDelimiter)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.updateDelimiterButtonText()
|
2019-12-23 01:34:10 +01:00
|
|
|
self.frm.allowHTML.setChecked(self.mw.pm.profile.get("allowHTML", True))
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self.frm.importMode.currentIndexChanged, self.importModeChanged)
|
2019-12-23 01:34:10 +01:00
|
|
|
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get("importMode", 1))
|
2020-01-15 09:33:32 +01:00
|
|
|
self.frm.tagModified.setText(self.mw.pm.profile.get("tagModified", ""))
|
|
|
|
self.frm.tagModified.setCol(self.mw.col)
|
2014-02-22 11:30:32 +01:00
|
|
|
# import button
|
2021-03-26 04:48:26 +01:00
|
|
|
b = QPushButton(tr.actions_import())
|
2014-02-22 11:30:32 +01:00
|
|
|
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.exec_()
|
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def setupOptions(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.model = self.mw.col.models.current()
|
|
|
|
self.modelChooser = aqt.modelchooser.ModelChooser(
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw, self.frm.modelArea, label=False
|
|
|
|
)
|
|
|
|
self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def modelChanged(self, unused: Any = None) -> None:
|
2021-06-27 04:12:23 +02:00
|
|
|
self.importer.note_type = self.mw.col.models.current()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.importer.initMapping()
|
|
|
|
self.showMapping()
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def onDelimiter(self) -> None:
|
2021-03-04 19:39:43 +01:00
|
|
|
|
|
|
|
# Open a modal dialog to enter an delimiter
|
|
|
|
# Todo/Idea Constrain the maximum width, so it doesnt take up that much screen space
|
|
|
|
delim, ok = getText(
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.importing_by_default_anki_will_detect_the(),
|
2021-03-04 19:39:43 +01:00
|
|
|
self,
|
|
|
|
help=HelpPage.IMPORTING,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
|
|
|
|
2021-03-04 19:39:43 +01:00
|
|
|
# If the modal dialog has been confirmed, update the delimiter
|
|
|
|
if ok:
|
|
|
|
# Check if the entered value is valid and if not fallback to default
|
|
|
|
# at the moment every single character entry as well as '\t' is valid
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-03-04 19:39:43 +01:00
|
|
|
delim = delim if len(delim) > 0 else self._DEFAULT_FILE_DELIMITER
|
|
|
|
delim = delim.replace("\\t", "\t") # un-escape it
|
|
|
|
if len(delim) > 1:
|
|
|
|
showWarning(
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.importing_multicharacter_separators_are_not_supported_please()
|
2021-03-04 19:39:43 +01:00
|
|
|
)
|
|
|
|
return
|
|
|
|
self.hideMapping()
|
|
|
|
|
|
|
|
def updateDelim() -> None:
|
|
|
|
self.importer.delimiter = delim
|
|
|
|
self.importer.updateDelimiter()
|
|
|
|
self.updateDelimiterButtonText()
|
|
|
|
|
|
|
|
self.showMapping(hook=updateDelim)
|
|
|
|
|
|
|
|
else:
|
|
|
|
# If the operation has been canceled, do not do anything
|
|
|
|
pass
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def updateDelimiterButtonText(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if not self.importer.needDelimiter:
|
|
|
|
return
|
|
|
|
if self.importer.delimiter:
|
|
|
|
d = self.importer.delimiter
|
|
|
|
else:
|
|
|
|
d = self.importer.dialect.delimiter
|
|
|
|
if d == "\t":
|
2021-03-26 04:48:26 +01:00
|
|
|
d = tr.importing_tab()
|
2012-12-21 08:51:59 +01:00
|
|
|
elif d == ",":
|
2021-03-26 04:48:26 +01:00
|
|
|
d = tr.importing_comma()
|
2012-12-21 08:51:59 +01:00
|
|
|
elif d == " ":
|
2021-03-26 04:48:26 +01:00
|
|
|
d = tr.studying_space()
|
2012-12-21 08:51:59 +01:00
|
|
|
elif d == ";":
|
2021-03-26 04:48:26 +01:00
|
|
|
d = tr.importing_semicolon()
|
2012-12-21 08:51:59 +01:00
|
|
|
elif d == ":":
|
2021-03-26 04:48:26 +01:00
|
|
|
d = tr.importing_colon()
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2016-05-12 06:45:35 +02:00
|
|
|
d = repr(d)
|
2021-03-26 05:21:04 +01:00
|
|
|
txt = tr.importing_fields_separated_by(val=d)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.frm.autoDetect.setText(txt)
|
2017-02-13 13:12:19 +01:00
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def accept(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.importer.mapping = self.mapping
|
|
|
|
if not self.importer.mappingOk():
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.importing_the_first_field_of_the_note())
|
2012-12-21 08:51:59 +01:00
|
|
|
return
|
|
|
|
self.importer.importMode = self.frm.importMode.currentIndex()
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw.pm.profile["importMode"] = self.importer.importMode
|
2012-12-21 08:51:59 +01:00
|
|
|
self.importer.allowHTML = self.frm.allowHTML.isChecked()
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw.pm.profile["allowHTML"] = self.importer.allowHTML
|
2020-01-15 09:33:32 +01:00
|
|
|
self.importer.tagModified = self.frm.tagModified.text()
|
2020-01-02 15:01:44 +01:00
|
|
|
self.mw.pm.profile["tagModified"] = self.importer.tagModified
|
2021-06-04 12:37:45 +02:00
|
|
|
self.mw.col.set_aux_notetype_config(
|
2021-06-27 04:12:23 +02:00
|
|
|
self.importer.note_type["id"], "lastDeck", self.deck.selected_deck_id
|
2021-06-04 12:37:45 +02:00
|
|
|
)
|
2021-06-27 04:12:23 +02:00
|
|
|
self.mw.col.models.save(self.importer.note_type, updateReqs=False)
|
2020-05-31 03:24:33 +02:00
|
|
|
self.mw.progress.start()
|
2021-03-26 04:48:26 +01:00
|
|
|
self.mw.checkpoint(tr.actions_import())
|
2020-03-06 05:36:05 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def on_done(future: Future) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.progress.finish()
|
2020-03-06 05:36:05 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
future.result()
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
showUnicodeWarning()
|
|
|
|
return
|
|
|
|
except Exception as e:
|
2021-03-26 04:48:26 +01:00
|
|
|
msg = f"{tr.importing_failed_debug_info()}\n"
|
2020-03-06 05:36:05 +01:00
|
|
|
err = repr(str(e))
|
|
|
|
if "1-character string" in err:
|
|
|
|
msg += err
|
|
|
|
elif "invalidTempFolder" in err:
|
|
|
|
msg += self.mw.errorHandler.tempFolderMsg()
|
|
|
|
else:
|
|
|
|
msg += traceback.format_exc()
|
|
|
|
showText(msg)
|
|
|
|
return
|
|
|
|
else:
|
2021-03-26 04:48:26 +01:00
|
|
|
txt = f"{tr.importing_importing_complete()}\n"
|
2020-03-06 05:36:05 +01:00
|
|
|
if self.importer.log:
|
|
|
|
txt += "\n".join(self.importer.log)
|
|
|
|
self.close()
|
2021-03-22 02:26:49 +01:00
|
|
|
showText(txt, plain_text_edit=True)
|
2020-03-06 05:36:05 +01:00
|
|
|
self.mw.reset()
|
|
|
|
|
|
|
|
self.mw.taskman.run_in_background(self.importer.run, on_done)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def setupMappingFrame(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
# qt seems to have a bug with adding/removing from a grid, so we add
|
|
|
|
# to a separate object and add/remove that instead
|
|
|
|
self.frame = QFrame(self.frm.mappingArea)
|
|
|
|
self.frm.mappingArea.setWidget(self.frame)
|
|
|
|
self.mapbox = QVBoxLayout(self.frame)
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mapbox.setContentsMargins(0, 0, 0, 0)
|
2021-02-01 13:08:56 +01:00
|
|
|
self.mapwidget: Optional[QWidget] = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def hideMapping(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.frm.mappingGroup.hide()
|
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def showMapping(
|
|
|
|
self, keepMapping: bool = False, hook: Optional[Callable] = None
|
|
|
|
) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
if hook:
|
|
|
|
hook()
|
|
|
|
if not keepMapping:
|
|
|
|
self.mapping = self.importer.mapping
|
|
|
|
self.frm.mappingGroup.show()
|
|
|
|
assert self.importer.fields()
|
|
|
|
# set up the mapping grid
|
|
|
|
if self.mapwidget:
|
|
|
|
self.mapbox.removeWidget(self.mapwidget)
|
|
|
|
self.mapwidget.deleteLater()
|
|
|
|
self.mapwidget = QWidget()
|
|
|
|
self.mapbox.addWidget(self.mapwidget)
|
|
|
|
self.grid = QGridLayout(self.mapwidget)
|
|
|
|
self.mapwidget.setLayout(self.grid)
|
2019-12-23 01:34:10 +01:00
|
|
|
self.grid.setContentsMargins(3, 3, 3, 3)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.grid.setSpacing(6)
|
|
|
|
fields = self.importer.fields()
|
|
|
|
for num in range(len(self.mapping)):
|
2021-03-26 05:21:04 +01:00
|
|
|
text = tr.importing_field_of_file_is(val=num + 1)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.grid.addWidget(QLabel(text), num, 0)
|
|
|
|
if self.mapping[num] == "_tags":
|
2021-03-26 04:48:26 +01:00
|
|
|
text = tr.importing_mapped_to_tags()
|
2012-12-21 08:51:59 +01:00
|
|
|
elif self.mapping[num]:
|
2021-03-26 05:21:04 +01:00
|
|
|
text = tr.importing_mapped_to(val=self.mapping[num])
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2021-03-26 04:48:26 +01:00
|
|
|
text = tr.importing_ignored()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.grid.addWidget(QLabel(text), num, 1)
|
2021-03-26 04:48:26 +01:00
|
|
|
button = QPushButton(tr.importing_change())
|
2012-12-21 08:51:59 +01:00
|
|
|
self.grid.addWidget(button, num, 2)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-02 15:00:29 +01:00
|
|
|
def changeMappingNum(self, n: int) -> None:
|
2021-06-27 04:12:23 +02:00
|
|
|
f = ChangeMap(self.mw, self.importer.note_type, self.mapping[n]).getField()
|
2012-12-21 08:51:59 +01:00
|
|
|
try:
|
|
|
|
# make sure we don't have it twice
|
|
|
|
index = self.mapping.index(f)
|
|
|
|
self.mapping[index] = None
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
self.mapping[n] = f
|
|
|
|
if getattr(self.importer, "delimiter", False):
|
|
|
|
self.savedDelimiter = self.importer.delimiter
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def updateDelim() -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.importer.delimiter = self.savedDelimiter
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
self.showMapping(hook=updateDelim, keepMapping=True)
|
|
|
|
else:
|
|
|
|
self.showMapping(keepMapping=True)
|
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def reject(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.modelChooser.cleanup()
|
2018-03-01 05:20:30 +01:00
|
|
|
self.deck.cleanup()
|
2020-01-15 07:53:24 +01:00
|
|
|
gui_hooks.current_note_type_did_change.remove(self.modelChanged)
|
2012-12-21 08:51:59 +01:00
|
|
|
QDialog.reject(self)
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def helpRequested(self) -> None:
|
2021-01-25 14:45:47 +01:00
|
|
|
openHelp(HelpPage.IMPORTING)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-02 15:00:29 +01:00
|
|
|
def importModeChanged(self, newImportMode: int) -> None:
|
2020-01-03 07:32:44 +01:00
|
|
|
if newImportMode == 0:
|
2020-01-15 09:33:32 +01:00
|
|
|
self.frm.tagModified.setEnabled(True)
|
2020-01-03 07:32:44 +01:00
|
|
|
else:
|
2020-01-15 09:33:32 +01:00
|
|
|
self.frm.tagModified.setEnabled(False)
|
2020-01-03 07:32:44 +01:00
|
|
|
|
2013-05-03 14:29:25 +02:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def showUnicodeWarning() -> None:
|
2013-05-03 14:29:25 +02:00
|
|
|
"""Shorthand to show a standard warning."""
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.importing_selected_file_was_not_in_utf8())
|
2013-05-03 14:29:25 +02:00
|
|
|
|
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def onImport(mw: AnkiQt) -> None:
|
2021-03-26 04:28:21 +01:00
|
|
|
filt = ";;".join([x[0] for x in importing.importers(mw.col)])
|
2021-03-26 04:48:26 +01:00
|
|
|
file = getFile(mw, tr.actions_import(), None, key="import", filter=filt)
|
2012-12-21 08:51:59 +01:00
|
|
|
if not file:
|
|
|
|
return
|
2016-05-12 06:45:35 +02:00
|
|
|
file = str(file)
|
2018-09-27 03:35:21 +02:00
|
|
|
|
|
|
|
head, ext = os.path.splitext(file)
|
|
|
|
ext = ext.lower()
|
|
|
|
if ext == ".anki":
|
2021-03-26 04:48:26 +01:00
|
|
|
showInfo(tr.importing_anki_files_are_from_a_very())
|
2018-09-27 03:35:21 +02:00
|
|
|
return
|
|
|
|
elif ext == ".anki2":
|
2021-03-26 04:48:26 +01:00
|
|
|
showInfo(tr.importing_anki2_files_are_not_directly_importable())
|
2018-09-27 03:35:21 +02:00
|
|
|
return
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
importFile(mw, file)
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def importFile(mw: AnkiQt, file: str) -> None:
|
2019-03-04 02:22:40 +01:00
|
|
|
importerClass = None
|
2012-12-21 08:51:59 +01:00
|
|
|
done = False
|
2021-03-26 04:28:21 +01:00
|
|
|
for i in importing.importers(mw.col):
|
2012-12-21 08:51:59 +01:00
|
|
|
if done:
|
|
|
|
break
|
2019-03-04 08:03:43 +01:00
|
|
|
for mext in re.findall(r"[( ]?\*\.(.+?)[) ]", i[0]):
|
2021-02-11 01:09:06 +01:00
|
|
|
if file.endswith(f".{mext}"):
|
2019-03-04 02:22:40 +01:00
|
|
|
importerClass = i[1]
|
2012-12-21 08:51:59 +01:00
|
|
|
done = True
|
|
|
|
break
|
2019-03-04 02:22:40 +01:00
|
|
|
if not importerClass:
|
2012-12-21 08:51:59 +01:00
|
|
|
# if no matches, assume TSV
|
2021-03-26 04:28:21 +01:00
|
|
|
importerClass = importing.importers(mw.col)[0][1]
|
2019-03-04 02:22:40 +01:00
|
|
|
importer = importerClass(mw.col, file)
|
2012-12-21 08:51:59 +01:00
|
|
|
# need to show import dialog?
|
|
|
|
if importer.needMapper:
|
|
|
|
# make sure we can load the file first
|
|
|
|
mw.progress.start(immediate=True)
|
|
|
|
try:
|
|
|
|
importer.open()
|
2020-05-29 00:40:03 +02:00
|
|
|
mw.progress.finish()
|
2020-05-12 16:15:01 +02:00
|
|
|
diag = ImportDialog(mw, importer)
|
2012-12-21 08:51:59 +01:00
|
|
|
except UnicodeDecodeError:
|
2020-03-14 09:42:03 +01:00
|
|
|
mw.progress.finish()
|
2013-05-03 14:29:25 +02:00
|
|
|
showUnicodeWarning()
|
2012-12-21 08:51:59 +01:00
|
|
|
return
|
2016-05-12 06:45:35 +02:00
|
|
|
except Exception as e:
|
2020-03-14 09:42:03 +01:00
|
|
|
mw.progress.finish()
|
2013-10-20 03:57:42 +02:00
|
|
|
msg = repr(str(e))
|
2014-06-17 15:31:38 +02:00
|
|
|
if msg == "'unknownFormat'":
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.importing_unknown_file_format())
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2021-03-26 04:48:26 +01:00
|
|
|
msg = f"{tr.importing_failed_debug_info()}\n"
|
2016-05-12 06:45:35 +02:00
|
|
|
msg += str(traceback.format_exc())
|
2012-12-21 08:51:59 +01:00
|
|
|
showText(msg)
|
|
|
|
return
|
|
|
|
finally:
|
2020-05-12 16:15:01 +02:00
|
|
|
importer.close()
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2013-09-11 08:56:59 +02:00
|
|
|
# if it's an apkg/zip, first test it's a valid file
|
2021-03-26 04:28:21 +01:00
|
|
|
if isinstance(importer, AnkiPackageImporter):
|
2013-09-11 08:56:59 +02:00
|
|
|
try:
|
2014-09-27 17:19:43 +02:00
|
|
|
z = zipfile.ZipFile(importer.file)
|
2013-09-11 08:56:59 +02:00
|
|
|
z.getinfo("collection.anki2")
|
|
|
|
except:
|
2014-06-28 22:55:01 +02:00
|
|
|
showWarning(invalidZipMsg())
|
2013-09-11 08:56:59 +02:00
|
|
|
return
|
2020-03-06 05:36:05 +01:00
|
|
|
# we need to ask whether to import/replace; if it's
|
|
|
|
# a colpkg file then the rest of the import process
|
|
|
|
# will happen in setupApkgImport()
|
2012-12-21 08:51:59 +01:00
|
|
|
if not setupApkgImport(mw, importer):
|
|
|
|
return
|
2020-03-06 05:36:05 +01:00
|
|
|
|
|
|
|
# importing non-colpkg files
|
2012-12-21 08:51:59 +01:00
|
|
|
mw.progress.start(immediate=True)
|
2020-03-06 05:38:35 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def on_done(future: Future) -> None:
|
2020-03-06 05:38:35 +01:00
|
|
|
mw.progress.finish()
|
2018-12-01 04:37:26 +01:00
|
|
|
try:
|
2020-03-06 05:38:35 +01:00
|
|
|
future.result()
|
|
|
|
except zipfile.BadZipfile:
|
|
|
|
showWarning(invalidZipMsg())
|
|
|
|
except Exception as e:
|
|
|
|
err = repr(str(e))
|
|
|
|
if "invalidFile" in err:
|
2021-03-26 04:48:26 +01:00
|
|
|
msg = tr.importing_invalid_file_please_restore_from_backup()
|
2020-03-06 05:38:35 +01:00
|
|
|
showWarning(msg)
|
|
|
|
elif "invalidTempFolder" in err:
|
|
|
|
showWarning(mw.errorHandler.tempFolderMsg())
|
|
|
|
elif "readonly" in err:
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.importing_unable_to_import_from_a_readonly())
|
2020-03-06 05:38:35 +01:00
|
|
|
else:
|
2021-03-26 04:48:26 +01:00
|
|
|
msg = f"{tr.importing_failed_debug_info()}\n"
|
2020-03-06 05:38:35 +01:00
|
|
|
msg += str(traceback.format_exc())
|
|
|
|
showText(msg)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2020-03-06 05:38:35 +01:00
|
|
|
log = "\n".join(importer.log)
|
|
|
|
if "\n" not in log:
|
|
|
|
tooltip(log)
|
|
|
|
else:
|
2021-02-02 06:47:51 +01:00
|
|
|
showText(log, plain_text_edit=True)
|
2020-03-06 05:38:35 +01:00
|
|
|
|
|
|
|
mw.reset()
|
|
|
|
|
|
|
|
mw.taskman.run_in_background(importer.run, on_done)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def invalidZipMsg() -> str:
|
2021-03-26 04:48:26 +01:00
|
|
|
return tr.importing_this_file_does_not_appear_to()
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2014-06-28 22:55:01 +02:00
|
|
|
|
2021-02-01 13:08:56 +01:00
|
|
|
def setupApkgImport(mw: AnkiQt, importer: AnkiPackageImporter) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
base = os.path.basename(importer.file).lower()
|
2019-12-23 01:34:10 +01:00
|
|
|
full = (
|
|
|
|
(base == "collection.apkg")
|
|
|
|
or re.match("backup-.*\\.apkg", base)
|
|
|
|
or base.endswith(".colpkg")
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
if not full:
|
|
|
|
# adding
|
|
|
|
return True
|
2019-12-23 01:34:10 +01:00
|
|
|
if not mw.restoringBackup and not askUser(
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.importing_this_will_delete_your_existing_collection(),
|
2019-12-23 01:34:10 +01:00
|
|
|
msgfunc=QMessageBox.warning,
|
|
|
|
defaultno=True,
|
|
|
|
):
|
2012-12-21 08:51:59 +01:00
|
|
|
return False
|
2019-04-29 08:59:29 +02:00
|
|
|
|
|
|
|
replaceWithApkg(mw, importer.file, mw.restoringBackup)
|
2021-02-01 13:08:56 +01:00
|
|
|
return False
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-02 15:00:29 +01:00
|
|
|
def replaceWithApkg(mw: aqt.AnkiQt, file: str, backup: bool) -> None:
|
2017-08-16 11:45:39 +02:00
|
|
|
mw.unloadCollection(lambda: _replaceWithApkg(mw, file, backup))
|
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-02-02 15:00:29 +01:00
|
|
|
def _replaceWithApkg(mw: aqt.AnkiQt, filename: str, backup: bool) -> None:
|
2017-08-16 11:45:39 +02:00
|
|
|
mw.progress.start(immediate=True)
|
2018-01-29 05:12:04 +01:00
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def do_import() -> None:
|
2020-03-06 05:55:15 +01:00
|
|
|
z = zipfile.ZipFile(filename)
|
2018-01-29 05:12:04 +01:00
|
|
|
|
2020-03-06 05:55:15 +01:00
|
|
|
# v2 scheduler?
|
|
|
|
colname = "collection.anki21"
|
|
|
|
try:
|
|
|
|
z.getinfo(colname)
|
|
|
|
except KeyError:
|
|
|
|
colname = "collection.anki2"
|
2018-01-29 05:12:04 +01:00
|
|
|
|
2019-12-23 01:34:10 +01:00
|
|
|
with z.open(colname) as source, open(mw.pm.collectionPath(), "wb") as target:
|
2020-07-24 20:38:34 +02:00
|
|
|
# ignore appears related to https://github.com/python/typeshed/issues/4349
|
|
|
|
# see if can turn off once issue fix is merged in
|
2020-10-10 11:02:59 +02:00
|
|
|
shutil.copyfileobj(source, target)
|
2020-03-06 05:55:15 +01:00
|
|
|
|
|
|
|
d = os.path.join(mw.pm.profileFolder(), "collection.media")
|
|
|
|
for n, (cStr, file) in enumerate(
|
|
|
|
json.loads(z.read("media").decode("utf8")).items()
|
|
|
|
):
|
|
|
|
mw.taskman.run_on_main(
|
2021-02-02 15:00:29 +01:00
|
|
|
lambda n=n: mw.progress.update( # type: ignore
|
2021-03-26 05:21:04 +01:00
|
|
|
tr.importing_processed_media_file(count=n)
|
2020-03-06 05:55:15 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
size = z.getinfo(cStr).file_size
|
|
|
|
dest = os.path.join(d, unicodedata.normalize("NFC", file))
|
|
|
|
# if we have a matching file size
|
|
|
|
if os.path.exists(dest) and size == os.stat(dest).st_size:
|
|
|
|
continue
|
|
|
|
data = z.read(cStr)
|
2020-05-10 02:58:42 +02:00
|
|
|
with open(dest, "wb") as file:
|
|
|
|
file.write(data)
|
2020-03-06 05:55:15 +01:00
|
|
|
|
|
|
|
z.close()
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def on_done(future: Future) -> None:
|
2017-08-16 11:45:39 +02:00
|
|
|
mw.progress.finish()
|
2020-03-06 05:55:15 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
future.result()
|
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
2021-03-26 04:48:26 +01:00
|
|
|
showWarning(tr.importing_the_provided_file_is_not_a())
|
2020-03-06 05:55:15 +01:00
|
|
|
return
|
|
|
|
|
|
|
|
if not mw.loadCollection():
|
|
|
|
return
|
|
|
|
if backup:
|
|
|
|
mw.col.modSchema(check=False)
|
|
|
|
|
2021-03-26 04:48:26 +01:00
|
|
|
tooltip(tr.importing_importing_complete())
|
2020-03-06 05:55:15 +01:00
|
|
|
|
|
|
|
mw.taskman.run_in_background(do_import, on_done)
|