Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dataclasses = "*"
pygtrie = "*"
cached-property = "*"
PyYAML = "*"
pygithub = "*"

[requires]
python_version = "3.7"
19 changes: 19 additions & 0 deletions crowd_anki/config/config_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@ def accept(self):
def ui_initial_setup(self):
self.setup_snapshot_options()
self.setup_export_options()
self.setup_github_options()
self.setup_import_options()

def setup_snapshot_options(self):
self.form.textedit_snapshot_path.setText(self.config.snapshot_path)
self.form.textedit_snapshot_path.textChanged.connect(self.changed_textedit_snapshot_path)

self.form.textedit_gh_username.textChanged.connect(self.changed_gh_username)
self.form.textedit_gh_password.textChanged.connect(self.changed_gh_password)
self.form.textedit_gh_repo.textChanged.connect(self.changed_gh_repo)

self.form.cb_automated_snapshot.setChecked(self.config.automated_snapshot)
self.form.cb_automated_snapshot.stateChanged.connect(self.toggle_automated_snapshot)

Expand All @@ -61,6 +66,20 @@ def setup_import_options(self):
self.form.cb_ignore_move_cards.setChecked(self.config.import_notes_ignore_deck_movement)
self.form.cb_ignore_move_cards.stateChanged.connect(self.toggle_ignore_move_cards)

def setup_github_options(self):
self.form.textedit_gh_username.setText(self.config.gh_username)
self.form.textedit_gh_password.setText(self.config.gh_password)
self.form.textedit_gh_repo.setText(self.config.gh_repo)

def changed_gh_username(self):
self.config.gh_username = self.form.textedit_gh_username.text()

def changed_gh_password(self):
self.config.gh_password = self.form.textedit_gh_password.text()

def changed_gh_repo(self):
self.config.gh_repo = self.form.textedit_gh_repo

def toggle_automated_snapshot(self):
self.config.automated_snapshot = not self.config.automated_snapshot

Expand Down
8 changes: 8 additions & 0 deletions crowd_anki/config/config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class ConfigSettings:
export_create_deck_subdirectory: bool
import_notes_ignore_deck_movement: bool

gh_username: str
gh_password: str
gh_repo: str

@property
def formatted_export_note_sort_methods(self) -> list:
return [
Expand All @@ -49,6 +53,10 @@ class Properties(Enum):
EXPORT_CREATE_DECK_SUBDIRECTORY = ConfigEntry("export_create_deck_subdirectory", True)
IMPORT_NOTES_IGNORE_DECK_MOVEMENT = ConfigEntry("import_notes_ignore_deck_movement", False)

GH_USERNAME = ConfigEntry("gh_username", "")
GH_PASSWORD = ConfigEntry("gh_password", "")
GH_REPO = ConfigEntry("gh_password", "")

def __init__(self, addon_manager=None, init_values=None, profile_manager=None):
self._profile_manager = profile_manager or mw.pm
self.addon_manager = addon_manager or mw.addonManager
Expand Down
67 changes: 67 additions & 0 deletions crowd_anki/export/anki_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
from typing import Callable

from github import Github

from .deck_exporter import DeckExporter
from ..anki.adapters.anki_deck import AnkiDeck
Expand All @@ -14,6 +15,7 @@
from ..utils.filesystem.name_sanitizer import sanitize_anki_deck_name
from .note_sorter import NoteSorter
from ..config.config_settings import ConfigSettings
from ..utils.notifier import AnkiModalNotifier, Notifier


class AnkiJsonExporter(DeckExporter):
Expand Down Expand Up @@ -54,6 +56,71 @@ def export_to_directory(self, deck: AnkiDeck, output_dir=Path("."), copy_media=T
self._copy_media(deck, deck_directory)

return deck_directory

def export_to_github(self, deck: AnkiDeck, user, pass, repo, copy_media=True, create_deck_subdirectory=True, notifier=None):

"""
This utility function directly uploads an AnkiDeck to Github in the JSON format.

To authorize it, a username and password must be supplied (note: password should be a Github
personal access token from https://github.com/settings/tokens - don't try it using your
actual username and password as that would be highly insecure and against best practices.

Note: if a file already exists on the repo at the location determined, it will be updated.
"""

deck_directory = ""
if create_deck_subdirectory:
deck_directory = f"{self.deck_name_sanitizer(deck.name)}/"

filename = deck_directory + self.deck_file_name + DECK_FILE_EXTENSION

deck = deck_initializer.from_collection(self.collection, deck.name)
deck.notes = self.note_sorter.sort_notes(deck.notes)
self.last_exported_count = deck.get_note_count()

g = Github(user, pass)

try:
gh_user = g.get_user()
except:
return notifier.warning("Authentication to Github failed", "Authenticating with Github failed. Please check that "
"both your username and password are correct. Remember: don't use your "
"real Github login password, create a personal access token (https://git.io/token) "
"and use that as the password.")

# We find out if the file exists so we can replace it
# Code snippet from https://stackoverflow.com/a/63445581, CC-BY-SA

try:
repo = gh_user.get_repo(GITHUB_REPO)
except:
return notifier.warning("Unable to find Github repository", "Unable to find your Github repository. Make sure you've created one first: https://repo.new")

all_files = []
contents = repo.get_contents("")

while contents:
file_content = contents.pop(0)
if file_content.type == "dir":
contents.extend(repo.get_contents(file_content.path))
else:
file = file_content
all_files.append(str(file).replace('ContentFile(path="','').replace('")',''))

try:
if filename in all_files:
contents = repo.get_contents(filename)
new_contents = json.dumps(deck, default=Deck.default_json, sort_keys=True, indent=4, ensure_ascii=False)
repo.update_file(contents.path, "Automated update from CrowdAnki", new_contents, contents.sha, branch = "main")
else:
new_contents = json.dumps(deck, default=Deck.default_json, sort_keys=True, indent=4, ensure_ascii=False)
repo.create_file(filename, "Automated upload from CrowdAnki", new_contents, branch="main")
except Exception as e:
return notifier.warning("Unknown error when uploading file", "Please report this error at https://git.io/JCUKl.\n\n" + str(e))

# Not sure what to return if successful
return True

def _save_changes(self, deck, is_export_child=False):
"""Save updates that were made during the export. E.g. UUID fields
Expand Down
31 changes: 29 additions & 2 deletions crowd_anki/export/anki_exporter_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from crowd_anki import config
from pathlib import Path

from .anki_exporter import AnkiJsonExporter
Expand All @@ -23,14 +24,40 @@ class AnkiJsonExporterWrapper:
def __init__(self, collection,
deck_id: int = None,
json_exporter: AnkiJsonExporter = None,
gh_username: str = None,
gh_password: str = None,
gh_repo: str = None,
notifier: Notifier = None):

self.gh_username = gh_username
self.gh_password = gh_password
self.gh_repo = gh_repo

self.includeMedia = True
self.did = deck_id
self.count = 0 # Todo?
self.collection = collection
self.anki_json_exporter = json_exporter or AnkiJsonExporter(collection, ConfigSettings.get_instance())
self.notifier = notifier or AnkiModalNotifier()

def exportToGithub(self):
if self.did is None:
self.notifier.warning(EXPORT_FAILED_TITLE, "CrowdAnki export works only for specific decks. "
"Please use CrowdAnki snapshot if you want to export "
"the whole collection.")
return

deck = AnkiDeck(self.collection.decks.get(self.did, default=False))
if deck.is_dynamic:
self.notifier.warning(EXPORT_FAILED_TITLE, "CrowdAnki does not support export for dynamic decks.")
return

self.anki_json_exporter.export_to_github(deck, ConfigSettings.get_instance().gh_username,
ConfigSettings.get_instance().gh_password, self.includeMedia,
ConfigSettings.get_instance().gh_repo, create_deck_subdirectory=ConfigSettings.get_instance().export_create_deck_subdirectory)

self.count = self.anki_json_exporter.last_exported_count

# required by anki exporting interface with its non-PEP-8 names
# noinspection PyPep8Naming
def exportInto(self, directory_path):
Expand All @@ -48,11 +75,11 @@ def exportInto(self, directory_path):
# .parent because we receive name with random numbers at the end (hacking around internals of Anki) :(
export_path = Path(directory_path).parent
self.anki_json_exporter.export_to_directory(deck, export_path, self.includeMedia,
create_deck_subdirectory=ConfigSettings.get_instance().export_create_deck_subdirectory)
create_deck_subdirectory=ConfigSettings.get_instance().export_create_deck_subdirectory,
notifier=self.notifier)

self.count = self.anki_json_exporter.last_exported_count


def get_exporter_id(exporter):
return f"{exporter.key} (*{exporter.ext})", exporter

Expand Down
56 changes: 56 additions & 0 deletions ui_files/config.ui
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,62 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="group_snapshot">
<property name="title">
<string>Github</string>
</property>

<layout class="QVBoxLayout" name="verticalLayout_3">

<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="lbl_gh_username">
<property name="text">
<string>Github username (no @):</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="textedit_gh_username"/>
</item>
</layout>
</item>

<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="lbl_gh_password">
<property name="text">
<string>Github personal access token (from https://git.io/token):</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="textedit_gh_password"/>
</item>
</layout>
</item>

<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="lbl_gh_repo">
<property name="text">
<string>Github export repository (from https://repo.new):</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="textedit_gh_repo"/>
</item>
</layout>
</item>

</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="group_deck_import">
<property name="title">
Expand Down