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
|
|
|
|
|
2020-02-11 08:30:10 +01:00
|
|
|
import itertools
|
2020-02-10 08:58:54 +01:00
|
|
|
import time
|
|
|
|
from concurrent.futures import Future
|
2021-10-03 10:59:42 +02:00
|
|
|
from typing import Iterable, Sequence, TypeVar
|
2020-02-10 08:58:54 +01:00
|
|
|
|
|
|
|
import aqt
|
2021-02-11 10:57:19 +01:00
|
|
|
from anki.collection import SearchNode
|
2021-01-31 06:55:08 +01:00
|
|
|
from anki.errors import Interrupted
|
2021-06-20 07:49:20 +02:00
|
|
|
from anki.media import CheckMediaResponse
|
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
|
|
|
|
2020-02-11 08:30:10 +01:00
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def chunked_list(l: Iterable[T], n: int) -> Iterable[list[T]]:
|
2020-02-11 08:30:10 +01:00
|
|
|
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:
|
2021-10-03 10:59:42 +02:00
|
|
|
progress_dialog: aqt.progress.ProgressDialog | None
|
2020-02-10 08:58:54 +01:00
|
|
|
|
|
|
|
def __init__(self, mw: aqt.AnkiQt) -> None:
|
|
|
|
self.mw = mw
|
2021-10-03 10:59:42 +02:00
|
|
|
self._progress_timer: QTimer | None = None
|
2020-02-10 08:58:54 +01:00
|
|
|
|
|
|
|
def check(self) -> None:
|
|
|
|
self.progress_dialog = self.mw.progress.start()
|
2020-05-29 11:59:50 +02:00
|
|
|
self._set_progress_enabled(True)
|
2020-02-10 08:58:54 +01:00
|
|
|
self.mw.taskman.run_in_background(self._check, self._on_finished)
|
|
|
|
|
2020-05-29 11:59:50 +02:00
|
|
|
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()
|
2020-05-29 11:59:50 +02:00
|
|
|
|
|
|
|
def _on_progress(self) -> None:
|
2021-02-08 07:46:57 +01:00
|
|
|
if not self.mw.col:
|
|
|
|
return
|
2020-05-29 11:59:50 +02:00
|
|
|
progress = self.mw.col.latest_progress()
|
2021-02-08 07:40:27 +01:00
|
|
|
if not progress.HasField("media_check"):
|
2020-05-29 11:59:50 +02:00
|
|
|
return
|
2021-02-08 07:40:27 +01:00
|
|
|
label = progress.media_check
|
2020-05-29 11:59:50 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
if self.progress_dialog.wantCancel:
|
2021-01-31 09:46:43 +01:00
|
|
|
self.mw.col.set_wants_abort()
|
2020-05-29 11:59:50 +02:00
|
|
|
except AttributeError:
|
|
|
|
# dialog may not be active
|
|
|
|
pass
|
2020-02-10 08:58:54 +01:00
|
|
|
|
2021-02-08 07:40:27 +01:00
|
|
|
self.mw.taskman.run_on_main(lambda: self.mw.progress.update(label=label))
|
2020-02-10 08:58:54 +01:00
|
|
|
|
2021-06-20 07:49:20 +02:00
|
|
|
def _check(self) -> CheckMediaResponse:
|
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:
|
2020-05-29 11:59:50 +02:00
|
|
|
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
|
|
|
|
|
2021-06-20 07:49:20 +02:00
|
|
|
output: CheckMediaResponse = 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)
|
2021-03-26 04:48:26 +01:00
|
|
|
diag.setWindowTitle(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)
|
2021-02-08 07:42:21 +01:00
|
|
|
text = QPlainTextEdit()
|
2020-02-10 08:58:54 +01:00
|
|
|
text.setReadOnly(True)
|
|
|
|
text.setPlainText(report)
|
2021-10-05 05:53:01 +02:00
|
|
|
text.setWordWrapMode(QTextOption.WrapMode.NoWrap)
|
2020-02-10 08:58:54 +01:00
|
|
|
layout.addWidget(text)
|
2021-10-05 05:53:01 +02:00
|
|
|
box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
2020-02-10 08:58:54 +01:00
|
|
|
layout.addWidget(box)
|
2020-02-11 08:30:10 +01:00
|
|
|
|
2020-02-10 08:58:54 +01:00
|
|
|
if output.unused:
|
2021-03-26 04:48:26 +01:00
|
|
|
b = QPushButton(tr.media_check_delete_unused())
|
2020-02-10 08:58:54 +01:00
|
|
|
b.setAutoDefault(False)
|
2021-10-05 05:53:01 +02:00
|
|
|
box.addButton(b, QDialogButtonBox.ButtonRole.RejectRole)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(b.clicked, lambda c: self._on_trash_files(output.unused))
|
2020-02-10 08:58:54 +01:00
|
|
|
|
2020-02-11 06:09:33 +01:00
|
|
|
if output.missing:
|
|
|
|
if any(map(lambda x: x.startswith("latex-"), output.missing)):
|
2021-03-26 04:48:26 +01:00
|
|
|
b = QPushButton(tr.media_check_render_latex())
|
2020-02-11 06:09:33 +01:00
|
|
|
b.setAutoDefault(False)
|
2021-10-05 05:53:01 +02:00
|
|
|
box.addButton(b, QDialogButtonBox.ButtonRole.RejectRole)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(b.clicked, self._on_render_latex)
|
2020-02-11 06:09:33 +01:00
|
|
|
|
2020-03-10 03:49:40 +01:00
|
|
|
if output.have_trash:
|
2021-03-26 04:48:26 +01:00
|
|
|
b = QPushButton(tr.media_check_empty_trash())
|
2020-03-10 03:49:40 +01:00
|
|
|
b.setAutoDefault(False)
|
2021-10-05 05:53:01 +02:00
|
|
|
box.addButton(b, QDialogButtonBox.ButtonRole.RejectRole)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(b.clicked, lambda c: self._on_empty_trash())
|
2020-03-10 04:35:09 +01:00
|
|
|
|
2021-03-26 04:48:26 +01:00
|
|
|
b = QPushButton(tr.media_check_restore_trash())
|
2020-03-10 04:35:09 +01:00
|
|
|
b.setAutoDefault(False)
|
2021-10-05 05:53:01 +02:00
|
|
|
box.addButton(b, QDialogButtonBox.ButtonRole.RejectRole)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(b.clicked, lambda c: self._on_restore_trash())
|
2020-03-10 04:35:09 +01:00
|
|
|
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(box.rejected, diag.reject)
|
2020-02-10 08:58:54 +01:00
|
|
|
diag.setMinimumHeight(400)
|
|
|
|
diag.setMinimumWidth(500)
|
|
|
|
restoreGeom(diag, "checkmediadb")
|
2021-10-05 02:01:45 +02:00
|
|
|
diag.exec()
|
2020-02-10 08:58:54 +01:00
|
|
|
saveGeom(diag, "checkmediadb")
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def _on_render_latex(self) -> None:
|
2020-02-11 06:09:33 +01:00
|
|
|
self.progress_dialog = self.mw.progress.start()
|
|
|
|
try:
|
2020-02-11 06:36:05 +01:00
|
|
|
out = self.mw.col.media.render_all_latex(self._on_render_latex_progress)
|
2020-02-11 07:46:57 +01:00
|
|
|
if self.progress_dialog.wantCancel:
|
|
|
|
return
|
2020-02-11 06:09:33 +01:00
|
|
|
finally:
|
|
|
|
self.mw.progress.finish()
|
2020-02-11 07:46:57 +01:00
|
|
|
self.progress_dialog = None
|
2020-02-11 06:36:05 +01:00
|
|
|
|
|
|
|
if out is not None:
|
|
|
|
nid, err = out
|
2021-02-11 10:57:19 +01:00
|
|
|
aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),))
|
2020-02-11 06:36:05 +01:00
|
|
|
showText(err, type="html")
|
|
|
|
else:
|
2021-03-26 04:48:26 +01:00
|
|
|
tooltip(tr.media_check_all_latex_rendered())
|
2020-02-11 06:09:33 +01:00
|
|
|
|
|
|
|
def _on_render_latex_progress(self, count: int) -> bool:
|
|
|
|
if self.progress_dialog.wantCancel:
|
|
|
|
return False
|
|
|
|
|
2021-03-26 05:21:04 +01:00
|
|
|
self.mw.progress.update(tr.media_check_checked(count=count))
|
2020-02-11 06:09:33 +01:00
|
|
|
return True
|
|
|
|
|
2021-02-01 14:28:21 +01:00
|
|
|
def _on_trash_files(self, fnames: Sequence[str]) -> None:
|
2021-03-26 04:48:26 +01:00
|
|
|
if not askUser(tr.media_check_delete_unused_confirm()):
|
2020-02-11 08:30:10 +01:00
|
|
|
return
|
|
|
|
|
|
|
|
self.progress_dialog = self.mw.progress.start()
|
|
|
|
|
|
|
|
last_progress = time.time()
|
|
|
|
remaining = len(fnames)
|
2020-02-17 01:18:20 +01:00
|
|
|
total = len(fnames)
|
2020-02-11 08:30:10 +01:00
|
|
|
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:
|
2020-02-17 01:18:20 +01:00
|
|
|
self.mw.progress.update(
|
2021-03-26 05:21:04 +01:00
|
|
|
tr.media_check_files_remaining(count=remaining)
|
2020-02-11 08:30:10 +01:00
|
|
|
)
|
|
|
|
finally:
|
|
|
|
self.mw.progress.finish()
|
|
|
|
self.progress_dialog = None
|
|
|
|
|
2021-03-26 05:21:04 +01:00
|
|
|
tooltip(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()
|
2020-05-29 11:59:50 +02:00
|
|
|
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:
|
2021-01-31 09:46:43 +01:00
|
|
|
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()
|
2020-05-29 11:59:50 +02:00
|
|
|
self._set_progress_enabled(False)
|
2020-03-10 03:49:40 +01:00
|
|
|
# check for errors
|
|
|
|
fut.result()
|
|
|
|
|
2021-03-26 04:48:26 +01:00
|
|
|
tooltip(tr.media_check_trash_emptied())
|
2020-03-10 03:49:40 +01:00
|
|
|
|
|
|
|
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()
|
2020-05-29 11:59:50 +02:00
|
|
|
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:
|
2021-01-31 09:46:43 +01:00
|
|
|
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()
|
2020-05-29 11:59:50 +02:00
|
|
|
self._set_progress_enabled(False)
|
2020-03-10 04:35:09 +01:00
|
|
|
# check for errors
|
|
|
|
fut.result()
|
|
|
|
|
2021-03-26 04:48:26 +01:00
|
|
|
tooltip(tr.media_check_trash_restored())
|
2020-03-10 04:35:09 +01:00
|
|
|
|
|
|
|
self.mw.taskman.run_in_background(restore_trash, on_done)
|