anki/qt/aqt/mediacheck.py

225 lines
7.0 KiB
Python
Raw Normal View History

2020-02-10 08:58:54 +01:00
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import itertools
2020-02-10 08:58:54 +01:00
import time
from concurrent.futures import Future
from typing import Iterable, List, Optional, Sequence, TypeVar
2020-02-10 08:58:54 +01:00
import aqt
from anki.collection import SearchTerm
from anki.errors import Interrupted
from anki.lang import TR
from anki.media import CheckMediaOut
2020-02-10 08:58:54 +01:00
from aqt.qt import *
2021-01-07 05:46:55 +01:00
from aqt.utils import (
askUser,
disable_help_button,
restoreGeom,
saveGeom,
showText,
tooltip,
tr,
)
2020-02-10 08:58:54 +01:00
T = TypeVar("T")
def chunked_list(l: Iterable[T], n: int) -> Iterable[List[T]]:
l = iter(l)
while True:
res = list(itertools.islice(l, n))
if not res:
return
yield res
2020-02-10 08:58:54 +01:00
def check_media_db(mw: aqt.AnkiQt) -> None:
c = MediaChecker(mw)
c.check()
class MediaChecker:
progress_dialog: Optional[aqt.progress.ProgressDialog]
def __init__(self, mw: aqt.AnkiQt) -> None:
self.mw = mw
self._progress_timer: Optional[QTimer] = None
2020-02-10 08:58:54 +01:00
def check(self) -> None:
self.progress_dialog = self.mw.progress.start()
self._set_progress_enabled(True)
2020-02-10 08:58:54 +01:00
self.mw.taskman.run_in_background(self._check, self._on_finished)
def _set_progress_enabled(self, enabled: bool) -> None:
if self._progress_timer:
self._progress_timer.stop()
self._progress_timer = None
if enabled:
2021-02-08 07:46:57 +01:00
self._progress_timer = timer = QTimer()
timer.setSingleShot(False)
timer.setInterval(100)
qconnect(timer.timeout, self._on_progress)
timer.start()
def _on_progress(self) -> None:
2021-02-08 07:46:57 +01:00
if not self.mw.col:
return
progress = self.mw.col.latest_progress()
if not progress.HasField("media_check"):
return
label = progress.media_check
try:
if self.progress_dialog.wantCancel:
self.mw.col.set_wants_abort()
except AttributeError:
# dialog may not be active
pass
2020-02-10 08:58:54 +01:00
self.mw.taskman.run_on_main(lambda: self.mw.progress.update(label=label))
2020-02-10 08:58:54 +01:00
def _check(self) -> CheckMediaOut:
2020-02-10 08:58:54 +01:00
"Run the check on a background thread."
return self.mw.col.media.check()
2020-02-14 07:15:18 +01:00
def _on_finished(self, future: Future) -> None:
self._set_progress_enabled(False)
2020-02-10 08:58:54 +01:00
self.mw.progress.finish()
self.progress_dialog = None
exc = future.exception()
if isinstance(exc, Interrupted):
return
output: CheckMediaOut = future.result()
2020-02-14 07:15:18 +01:00
report = output.report
2020-02-10 08:58:54 +01:00
# show report and offer to delete
diag = QDialog(self.mw)
diag.setWindowTitle(tr(TR.MEDIA_CHECK_WINDOW_TITLE))
2021-01-07 05:46:55 +01:00
disable_help_button(diag)
2020-02-10 08:58:54 +01:00
layout = QVBoxLayout(diag)
diag.setLayout(layout)
text = QPlainTextEdit()
2020-02-10 08:58:54 +01:00
text.setReadOnly(True)
text.setPlainText(report)
text.setWordWrapMode(QTextOption.NoWrap)
2020-02-10 08:58:54 +01:00
layout.addWidget(text)
box = QDialogButtonBox(QDialogButtonBox.Close)
layout.addWidget(box)
2020-02-10 08:58:54 +01:00
if output.unused:
2020-02-27 03:25:19 +01:00
b = QPushButton(tr(TR.MEDIA_CHECK_DELETE_UNUSED))
2020-02-10 08:58:54 +01:00
b.setAutoDefault(False)
box.addButton(b, QDialogButtonBox.RejectRole)
qconnect(b.clicked, lambda c: self._on_trash_files(output.unused))
2020-02-10 08:58:54 +01:00
if output.missing:
if any(map(lambda x: x.startswith("latex-"), output.missing)):
2020-02-27 03:25:19 +01:00
b = QPushButton(tr(TR.MEDIA_CHECK_RENDER_LATEX))
b.setAutoDefault(False)
box.addButton(b, QDialogButtonBox.RejectRole)
qconnect(b.clicked, self._on_render_latex)
2020-03-10 03:49:40 +01:00
if output.have_trash:
b = QPushButton(tr(TR.MEDIA_CHECK_EMPTY_TRASH))
b.setAutoDefault(False)
box.addButton(b, QDialogButtonBox.RejectRole)
qconnect(b.clicked, lambda c: self._on_empty_trash())
2020-03-10 04:35:09 +01:00
b = QPushButton(tr(TR.MEDIA_CHECK_RESTORE_TRASH))
b.setAutoDefault(False)
box.addButton(b, QDialogButtonBox.RejectRole)
qconnect(b.clicked, lambda c: self._on_restore_trash())
2020-03-10 04:35:09 +01:00
qconnect(box.rejected, diag.reject)
2020-02-10 08:58:54 +01:00
diag.setMinimumHeight(400)
diag.setMinimumWidth(500)
restoreGeom(diag, "checkmediadb")
diag.exec_()
saveGeom(diag, "checkmediadb")
2021-02-01 14:28:21 +01:00
def _on_render_latex(self) -> None:
self.progress_dialog = self.mw.progress.start()
try:
out = self.mw.col.media.render_all_latex(self._on_render_latex_progress)
if self.progress_dialog.wantCancel:
return
finally:
self.mw.progress.finish()
self.progress_dialog = None
if out is not None:
nid, err = out
aqt.dialogs.open("Browser", self.mw, search=(SearchTerm(nid=nid),))
showText(err, type="html")
else:
2020-02-27 03:25:19 +01:00
tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED))
def _on_render_latex_progress(self, count: int) -> bool:
if self.progress_dialog.wantCancel:
return False
2020-02-27 03:25:19 +01:00
self.mw.progress.update(tr(TR.MEDIA_CHECK_CHECKED, count=count))
return True
2021-02-01 14:28:21 +01:00
def _on_trash_files(self, fnames: Sequence[str]) -> None:
2020-02-27 03:25:19 +01:00
if not askUser(tr(TR.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)):
return
self.progress_dialog = self.mw.progress.start()
last_progress = time.time()
remaining = len(fnames)
total = len(fnames)
try:
for chunk in chunked_list(fnames, 25):
self.mw.col.media.trash_files(chunk)
remaining -= len(chunk)
if time.time() - last_progress >= 0.3:
self.mw.progress.update(
2020-02-27 03:25:19 +01:00
tr(TR.MEDIA_CHECK_FILES_REMAINING, count=remaining)
)
finally:
self.mw.progress.finish()
self.progress_dialog = None
2020-02-27 03:25:19 +01:00
tooltip(tr(TR.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total))
2020-03-10 03:49:40 +01:00
2021-02-01 14:28:21 +01:00
def _on_empty_trash(self) -> None:
2020-03-10 03:49:40 +01:00
self.progress_dialog = self.mw.progress.start()
self._set_progress_enabled(True)
2020-03-10 03:49:40 +01:00
2021-02-01 14:28:21 +01:00
def empty_trash() -> None:
self.mw.col.media.empty_trash()
2020-03-10 03:49:40 +01:00
2021-02-01 14:28:21 +01:00
def on_done(fut: Future) -> None:
2020-03-10 03:49:40 +01:00
self.mw.progress.finish()
self._set_progress_enabled(False)
2020-03-10 03:49:40 +01:00
# check for errors
fut.result()
tooltip(tr(TR.MEDIA_CHECK_TRASH_EMPTIED))
self.mw.taskman.run_in_background(empty_trash, on_done)
2020-03-10 04:35:09 +01:00
2021-02-01 14:28:21 +01:00
def _on_restore_trash(self) -> None:
2020-03-10 04:35:09 +01:00
self.progress_dialog = self.mw.progress.start()
self._set_progress_enabled(True)
2020-03-10 04:35:09 +01:00
2021-02-01 14:28:21 +01:00
def restore_trash() -> None:
self.mw.col.media.restore_trash()
2020-03-10 04:35:09 +01:00
2021-02-01 14:28:21 +01:00
def on_done(fut: Future) -> None:
2020-03-10 04:35:09 +01:00
self.mw.progress.finish()
self._set_progress_enabled(False)
2020-03-10 04:35:09 +01:00
# check for errors
fut.result()
tooltip(tr(TR.MEDIA_CHECK_TRASH_RESTORED))
self.mw.taskman.run_in_background(restore_trash, on_done)