From ba00cf27f23dcead38fed8179fd90dc9a434254c Mon Sep 17 00:00:00 2001 From: azfoo <45888544+azfoo@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:24:09 +0100 Subject: [PATCH] feat: displaying remote pdfs --- tests/resources/example_multistaff.pdf | Bin 0 -> 18983 bytes tests/timelines/pdf/test_pdf_timeline.py | 34 ++++++++++++++++++++ tilia/app.py | 2 ++ tilia/dirs.py | 30 +++++++++++++++++- tilia/settings.py | 1 + tilia/timelines/pdf/timeline.py | 38 ++++++++++++++++++++--- tilia/ui/timelines/pdf/timeline.py | 5 ++- 7 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 tests/resources/example_multistaff.pdf diff --git a/tests/resources/example_multistaff.pdf b/tests/resources/example_multistaff.pdf new file mode 100644 index 0000000000000000000000000000000000000000..527b95ce32cd12d2d96ac8a4b506b77425f6695a GIT binary patch literal 18983 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 b8{m6pkf`$s!Y9lJ0S>4#Gb?D`!ufvynAtia literal 0 HcmV?d00001 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()),