diff --git a/tests/resources/example_multistaff.pdf b/tests/resources/example_multistaff.pdf new file mode 100644 index 000000000..527b95ce3 Binary files /dev/null and b/tests/resources/example_multistaff.pdf differ diff --git a/tests/timelines/pdf/test_pdf_timeline.py b/tests/timelines/pdf/test_pdf_timeline.py index d1ee32d35..a7f3e1887 100644 --- a/tests/timelines/pdf/test_pdf_timeline.py +++ b/tests/timelines/pdf/test_pdf_timeline.py @@ -1,4 +1,8 @@ +from pathlib import Path +from tilia.dirs import clear_tmp_path from tilia.ui import commands +from tilia.timelines.timeline_kinds import TimelineKind +from unittest.mock import patch class TestValidateComponentCreation: @@ -74,3 +78,33 @@ def test_page_number_is_limited_by_page_total(self, tilia_state, pdf_tl): tilia_state.current_time = 30 commands.execute("timeline.pdf.add") assert pdf_tl[-1].get_data("page_number") == 2 + + +class TestLoadPdf: + @patch("tilia.dirs.tmp_path", Path("tmp_path")) + def test_online_pdf(self, tls): + tls.create_timeline( + TimelineKind.PDF_TIMELINE, + path="https://s9.imslp.org/files/imglnks/usimg/0/04/IMSLP228371-WIMA.53e2-W.A.Moz.Ah_vous_dirai-je-Maman.pdf", + ) + + assert tls[0].is_pdf_valid + assert tls[0].page_total == 19 + assert not tls[0].is_local + + clear_tmp_path() + + def test_local_pdf(self, tls, resources): + tls.create_timeline( + TimelineKind.PDF_TIMELINE, + path=(resources / "example_multistaff.pdf").as_posix(), + ) + + assert tls[0].is_pdf_valid + assert tls[0].page_total == 1 + assert tls[0].is_local + + def test_non_pdf(self, tls, resources): + tls.create_timeline(TimelineKind.PDF_TIMELINE, path="nonexistent.pdf") + + assert not tls[0].is_pdf_valid diff --git a/tilia/app.py b/tilia/app.py index b4f7001ae..9e31b08a9 100644 --- a/tilia/app.py +++ b/tilia/app.py @@ -210,6 +210,8 @@ def on_close(self) -> None: return post(Post.UI_EXIT, 0) + if settings.get("general", "clear_cache_on_exit"): + tilia.dirs.clear_tmp_path() def load_media( self, diff --git a/tilia/dirs.py b/tilia/dirs.py index f2f2650fe..ece219854 100644 --- a/tilia/dirs.py +++ b/tilia/dirs.py @@ -8,6 +8,7 @@ autosaves_path = Path() logs_path = Path() +tmp_path = Path() _SITE_DATA_DIR = Path(platformdirs.site_data_dir(tilia.constants.APP_NAME)) _USER_DATA_DIR = Path( platformdirs.user_data_dir(tilia.constants.APP_NAME, roaming=True) @@ -39,12 +40,17 @@ def setup_logs_path(data_dir): create_logs_dir(data_dir) +def setup_tmp_path(data_dir): + if not os.path.exists(tmp_path): + create_tmp_path(data_dir) + + def setup_dirs() -> None: os.chdir(os.path.dirname(__file__)) data_dir = setup_data_dir() - global autosaves_path, logs_path + global autosaves_path, logs_path, tmp_path autosaves_path = Path(data_dir, "autosaves") setup_autosaves_path(data_dir) @@ -52,6 +58,9 @@ def setup_dirs() -> None: logs_path = Path(data_dir, "logs") setup_logs_path(data_dir) + tmp_path = Path(data_dir, "tmp") + setup_tmp_path(data_dir) + def create_data_dir() -> Path: try: @@ -72,5 +81,24 @@ def create_logs_dir(data_dir: Path): os.mkdir(Path(data_dir, "logs")) +def create_tmp_path(data_dir: Path): + os.mkdir(Path(data_dir, "tmp")) + + def open_autosaves_dir(): open_with_os(autosaves_path) + + +def clear_tmp_path(): + for root, dirs, files in os.walk(tmp_path, False): + r = Path(root) + for f in files: + try: + os.unlink(r / f) + except PermissionError: # file is in use + continue + for d in dirs: # dir is not empty + try: + os.rmdir(r / d) + except OSError: + continue diff --git a/tilia/settings.py b/tilia/settings.py index f311dfd73..cf26532b2 100644 --- a/tilia/settings.py +++ b/tilia/settings.py @@ -17,6 +17,7 @@ class SettingsManager(QObject): "timeline_background_color": "#EEE", "loop_box_shade": "#78c0c0c0", "prioritise_performance": "true", + "clear_cache_on_exit": "false", }, "auto-save": {"max_stored_files": 100, "interval_(seconds)": 300}, "media_metadata": { diff --git a/tilia/timelines/pdf/timeline.py b/tilia/timelines/pdf/timeline.py index a0013a044..0f6f6455d 100644 --- a/tilia/timelines/pdf/timeline.py +++ b/tilia/timelines/pdf/timeline.py @@ -2,8 +2,14 @@ import functools +import httpx import pypdf +from pathlib import Path +from urllib import parse + +import tilia.dirs +from tilia.errors import display, LOAD_FILE_ERROR from tilia.requests import get, Get from tilia.settings import settings from tilia.timelines.base.component.pointlike import scale_pointlike, crop_pointlike @@ -54,18 +60,40 @@ def path(self): return self._path @path.setter - def path(self, value): + def path(self, value: str): self._path = value - self.page_total = 0 + self.is_local = True self.is_pdf_valid = False - if checked_path := get(Get.VERIFIED_PATH, value): + self.page_total = 0 + if parse.urlparse(value).scheme in ("http", "https"): + path = tilia.dirs.tmp_path / value.partition("://")[2] + if not path.exists() and not self._download_pdf(path, value): + return + self.is_local = False + self.page_total = len(pypdf.PdfReader(path).pages) + self.is_pdf_valid = True + self.tmp_path = path.as_posix() + + elif checked_path := get(Get.VERIFIED_PATH, value): try: - self.page_total = len(pypdf.PdfReader(checked_path).pages) + self._path = Path(checked_path).as_posix() + self.is_local = True self.is_pdf_valid = True - self._path = checked_path + self.page_total = len(pypdf.PdfReader(checked_path).pages) + except FileNotFoundError: return + def _download_pdf(self, path, url): + if not (response := httpx.get(url)).is_success: + display(LOAD_FILE_ERROR, url, response) + return False + path = tilia.dirs.tmp_path / url.partition("://")[2] + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "wb") as f: + f.write(response.content) + return True + def setup_blank_timeline(self): self.create_component(ComponentKind.PDF_MARKER, time=0, page_number=1) diff --git a/tilia/ui/timelines/pdf/timeline.py b/tilia/ui/timelines/pdf/timeline.py index 896f24e9f..df520e44d 100644 --- a/tilia/ui/timelines/pdf/timeline.py +++ b/tilia/ui/timelines/pdf/timeline.py @@ -72,7 +72,10 @@ def _handle_invalid_pdf(self): def _load_pdf_file(self): if not self.timeline.get_data("is_pdf_valid"): self._handle_invalid_pdf() - self.pdf_document.load(self.get_data("path")) + if self.timeline.get_data("is_local"): + self.pdf_document.load(self.get_data("path")) + else: + self.pdf_document.load(self.get_data("tmp_path")) self.pdf_view.update_window( int(self.pdf_document.pagePointSize(0).height()), int(self.pdf_document.pagePointSize(0).width()),