Skip to content

Commit

Permalink
Add interface and app data folder detection
Browse files Browse the repository at this point in the history
  • Loading branch information
abdnh committed Sep 21, 2023
1 parent a52380c commit 2edfe24
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 35 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ So you can no longer export a zip of your cards [without paying](https://www.ank
This add-on salvages the cards from the SQLite database and was inspired by the Reddit post linked above.
It can import cards, decks, note types, and media files.

![The add-on's dialog](images/dialog.png)

## How to Use

- Download the add-on from https://ankiweb.net/shared/info/2072125761
- Make sure all your AnkiApp decks are downloaded before using the add-on. For that, go to AnkiApp, click on each of your decks, then click on the Download button at the bottom if it's shown.
- Run Anki and go to **Tools > Import From AnkiApp** and choose AnkiApp's database file (`C:\Users\%USERNAME%\AppData\Roaming\AnkiApp\databases\file__0` on Windows; `~/Library/Application Support/AnkiApp/databases/file__0/1` or under `~/Library/Containers/com.ankiapp.client/Data/Documents/ankiapp` on macOS).
- Grab a cup of coffee while waiting for importing to finish.
- Run Anki and go to **Tools > Import From AnkiApp**. The add-on tries to detect AnkiApp's data folder on your system automatically. If you see the "Data folder" field already populated, you can go ahead and click Import.

## Notes & Known Issues

Expand Down
6 changes: 4 additions & 2 deletions ankiweb_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

<b>How to Use</b>

<ul><li>Make sure all your AnkiApp decks are downloaded before using the add-on. For that, go to AnkiApp, click on each of your decks, then click on the Download button at the bottom if it's shown.</li><li>Go to <b>Tools > Import From AnkiApp</b> and choose AnkiApp's database file (<code>C:\Users\%USERNAME%\AppData\Roaming\AnkiApp\databases\file__0</code> on Windows; <code>~/Library/Application Support/AnkiApp/databases/file__0/1</code> or under <code>~/Library/Containers/com.ankiapp.client/Data/Documents/ankiapp</code> on macOS).</li><li>Grab a cup of coffee while waiting for importing to finish.</li></ul>

<ul>
<li>Make sure all your AnkiApp decks are downloaded before using the add-on. For that, go to AnkiApp, click on each of your decks, then click on the Download button at the bottom if it's shown.</li>
<li>Run Anki and go to <b>Tools &gt; Import From AnkiApp</b>. The add-on tries to detect AnkiApp's data folder on your system automatically. If you see the "Data folder" field already populated, you can go ahead and click Import.</li>
</ul>

<b>Notes &amp; Known Issues</b>

Expand Down
114 changes: 114 additions & 0 deletions designer/dialog.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>200</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string/>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QRadioButton" name="data_folder_checkbox">
<property name="toolTip">
<string>AnkiApp data folder</string>
</property>
<property name="text">
<string>Data folder</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="data_folder"/>
</item>
<item>
<widget class="QPushButton" name="choose_data_folder">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="database_file_checkbox">
<property name="toolTip">
<string>AnkiApp's SQLite database file. If you only have access to a single database file, you can use this, otherwise it's recommended to specify the data folder instead to allow the add-on to import more data</string>
</property>
<property name="text">
<string>Database file</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="database_file">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="choose_database_file">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="import_button">
<property name="text">
<string>Import</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="remote_media">
<property name="text">
<string>Try to download missing media from AnkiApp servers</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
Binary file added images/dialog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion requirements/bundle.in
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ankiutils @ git+https://github.com/abdnh/ankiutils@c699f193f35dcd5d8995ddc78b22d03642927018
ankiutils @ git+https://github.com/abdnh/ankiutils@a287daa1d0f84b08c623165d4ccdd4637e2d714c
2 changes: 1 addition & 1 deletion requirements/bundle.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ankiutils @ git+https://github.com/abdnh/ankiutils@c699f193f35dcd5d8995ddc78b22d03642927018
ankiutils @ git+https://github.com/abdnh/ankiutils@a287daa1d0f84b08c623165d4ccdd4637e2d714c
# via -r requirements/bundle.in
4 changes: 2 additions & 2 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ anki==2.1.66
# via aqt
ankiscripts @ git+https://github.com/abdnh/ankiscripts@1523f27088ff88cd0bca9cfb2ec9617eddc5e1e2
# via -r requirements/dev.in
ankiutils @ git+https://github.com/abdnh/ankiutils@c699f193f35dcd5d8995ddc78b22d03642927018
ankiutils @ git+https://github.com/abdnh/ankiutils@a287daa1d0f84b08c623165d4ccdd4637e2d714c
# via -r requirements\bundle.in
aqt==2.1.66
# via -r requirements/dev.in
Expand Down Expand Up @@ -156,7 +156,7 @@ pytest==7.4.2
# pytest-cov
pytest-cov==4.1.0
# via -r requirements/dev.in
pyupgrade==3.11.2
pyupgrade==3.12.0
# via -r requirements/dev.in
pywin32==306 ; sys_platform == "win32"
# via
Expand Down
40 changes: 15 additions & 25 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import os
import sys
from concurrent.futures import Future
from pathlib import Path
from textwrap import dedent
from typing import Optional

from aqt import mw
from aqt.gui_hooks import main_window_did_init
from aqt.qt import QAction, qconnect
from aqt.utils import getFile, showText, showWarning, tooltip
from aqt.utils import showText, showWarning, tooltip

sys.path.append(os.path.join(os.path.dirname(__file__), "vendor"))

Expand All @@ -17,17 +17,18 @@
AnkiAppImporterException,
)
from .consts import consts
from .gui.dialog import Dialog


def import_from_ankiapp(filename: str) -> None:
def import_from_ankiapp(db_path: Path) -> None:
mw.progress.start(
label="Extracting collection from AnkiApp database...",
immediate=True,
)
mw.progress.set_title(consts.name)

def start_importing() -> Optional[tuple[int, set[str]]]:
importer = AnkiAppImporter(mw, filename)
importer = AnkiAppImporter(mw, db_path)
return importer.import_to_anki(), importer.warnings

def on_done(fut: Future) -> None:
Expand Down Expand Up @@ -66,28 +67,17 @@ def on_done(fut: Future) -> None:
mw.taskman.run_in_background(start_importing, on_done)


def on_mw_init() -> None:
action = QAction(mw)
action.setText("Import From AnkiApp")
mw.form.menuTools.addAction(action)
action = QAction(mw)
action.setText("Import From AnkiApp")
mw.form.menuTools.addAction(action)

def on_triggered() -> None:
file = getFile(
mw,
"AnkiApp database file to import",
key="AnkiAppImporter",
cb=None,
filter="*",
)
if not file:
return
assert isinstance(file, str)
import_from_ankiapp(file)

qconnect(
action.triggered,
on_triggered,
)
def on_action() -> None:
dialog = Dialog(mw, on_done=import_from_ankiapp)
dialog.open()


main_window_did_init.append(on_mw_init)
qconnect(
action.triggered,
on_action,
)
5 changes: 3 additions & 2 deletions src/ankiapp_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import urllib
from collections.abc import Iterable, Iterator, MutableSet
from mimetypes import guess_extension
from pathlib import Path
from re import Match
from textwrap import dedent
from typing import Any, Dict, List, Optional, cast
Expand Down Expand Up @@ -236,7 +237,7 @@ class AnkiAppImporterCanceledException(AnkiAppImporterException):

# pylint: disable=too-few-public-methods,too-many-instance-attributes
class AnkiAppImporter:
def __init__(self, mw: AnkiQt, filename: str):
def __init__(self, mw: AnkiQt, db_path: Path):
self.mw = mw
self.BLOB_REF_PATTERNS = (
# Use Anki's HTML media patterns too for completeness
Expand All @@ -254,7 +255,7 @@ def __init__(self, mw: AnkiQt, filename: str):
),
)
self.config = mw.addonManager.getConfig(__name__)
self.con = sqlite3.connect(filename)
self.con = sqlite3.connect(db_path)
self._extract_notetypes()
self._extract_decks()
self._extract_media()
Expand Down
57 changes: 57 additions & 0 deletions src/appdata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import functools
import os
import sqlite3
from pathlib import Path
from typing import List, Optional, Union

try:
from anki.utils import is_mac, is_win
except ImportError:
from anki.utils import isMac as is_mac # type: ignore
from anki.utils import isWin as is_win # type: ignore


def get_ankiapp_data_folder() -> Optional[str]:
path = None
if is_win:
from aqt.winpaths import get_appdata

path = os.path.join(get_appdata(), "AnkiApp")
elif is_mac:
path = os.path.expanduser("~/Library/Application Support/AnkiApp")
if not os.path.exists(path):
# App store verison
path = os.path.expanduser(
"~/Library/Containers/com.ankiapp.client/Data/Documents/ankiapp"
)

if path is not None and os.path.exists(path):
return path
return None


# pylint: disable=too-few-public-methods
class AnkiAppData:
def __init__(self, path: Union[Path, str]):
self.path = Path(path)

@functools.cached_property
def sqlite_dbs(self) -> List[Path]:
databases_path = self.path / "databases"
databases_db_path = databases_path / "Databases.db"
if not databases_db_path.exists():
return []
with sqlite3.connect(databases_db_path) as conn:
db_paths = []
for row in conn.execute("select origin from Databases"):
# Use the first file found in the database subfolder
# TODO: inevstigate whether this can cause problems
db_path = next((databases_path / str(row[0])).iterdir(), None)
if db_path:
db_paths.append(db_path)
return db_paths


if __name__ == "__main__":
appdata = AnkiAppData(get_ankiapp_data_folder())
print(appdata.sqlite_dbs)
Empty file added src/gui/__init__.py
Empty file.
Loading

0 comments on commit 2edfe24

Please sign in to comment.