Skip to content

Commit e4a9255

Browse files
committed
Add PDF thumbnails to the _app
[skip ci]
1 parent edccf9b commit e4a9255

4 files changed

Lines changed: 127 additions & 6 deletions

File tree

‎scripts/gdrive_app.py‎

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,88 @@
1717
from gdrive import gcache
1818

1919
from PySide6.QtSvg import QSvgRenderer
20-
from PySide6.QtCore import QByteArray, Qt
21-
from PySide6.QtGui import QPainter
20+
from PySide6.QtCore import QByteArray, Qt, QRunnable, QObject, Signal, QThreadPool, Slot
21+
from PySide6.QtGui import QPainter, QImage
2222

23+
from collections import OrderedDict
24+
from functools import lru_cache
25+
import pdfutils as pdfutils
26+
import gdrive_base as gdrive_base
27+
from strutils import thumbnail_path_for_file
28+
29+
class ThumbnailSignals(QObject):
30+
result = Signal(str, QImage)
31+
32+
class ThumbnailWorker(QRunnable):
33+
def __init__(self, file_data):
34+
super().__init__()
35+
self.file_data = file_data
36+
self.signals = ThumbnailSignals()
37+
38+
@Slot()
39+
def run(self):
40+
file_id = self.file_data.get('id', '')
41+
mime = self.file_data.get('mimeType', '')
42+
img = None
43+
44+
try:
45+
from gdrive import gcache
46+
cache_path = gcache.get_cache_path_for_file(self.file_data)
47+
if cache_path and cache_path.exists():
48+
if mime == 'application/pdf':
49+
thumbnail_bytes = pdfutils.get_cached_pdf_thumbnail(cache_path, size='large')
50+
img = QImage()
51+
img.loadFromData(thumbnail_bytes)
52+
else:
53+
thumb_path = thumbnail_path_for_file(f"gdrive_{file_id}", size='large')
54+
if thumb_path.exists():
55+
img = QImage(str(thumb_path))
56+
else:
57+
if mime == 'application/pdf':
58+
thumbnail_bytes = gdrive_base.fetch_preview_image(file_id, size=256)
59+
if thumbnail_bytes:
60+
img = QImage()
61+
img.loadFromData(thumbnail_bytes)
62+
thumb_path.parent.mkdir(parents=True, exist_ok=True)
63+
thumb_path.write_bytes(thumbnail_bytes)
64+
except Exception as e:
65+
print(f"Error fetching thumbnail for {file_id}: {e}")
66+
67+
if img and not img.isNull():
68+
self.signals.result.emit(file_id, img)
69+
70+
class ThumbnailKickoffWorker(QRunnable):
71+
def __init__(self, items, callback):
72+
super().__init__()
73+
self.items = items
74+
self.callback = callback
75+
76+
@Slot()
77+
def run(self):
78+
pool = QThreadPool.globalInstance()
79+
for item in self.items:
80+
worker = ThumbnailWorker(item)
81+
worker.signals.result.connect(self.callback)
82+
pool.start(worker)
83+
84+
class LRUCache:
85+
def __init__(self, capacity: int):
86+
self.cache = OrderedDict()
87+
self.capacity = capacity
88+
89+
def get(self, key: str):
90+
if key not in self.cache:
91+
return None
92+
self.cache.move_to_end(key)
93+
return self.cache[key]
94+
95+
def put(self, key: str, value: Any):
96+
self.cache[key] = value
97+
self.cache.move_to_end(key)
98+
if len(self.cache) > self.capacity:
99+
self.cache.popitem(last=False)
100+
101+
@lru_cache(maxsize=None)
23102
def get_icon(icon_enum, is_filled=False, color: Optional[str] = None) -> QIcon:
24103
if color is None:
25104
if QApplication.instance():
@@ -86,6 +165,11 @@ def __init__(self):
86165
self.history_index = -1
87166
self.current_folder_id = None
88167

168+
self.threadpool = QThreadPool.globalInstance()
169+
self.threadpool.setMaxThreadCount(12)
170+
self.thumbnail_cache = LRUCache(500)
171+
self.item_mapping = {}
172+
89173
self.init_ui()
90174
self.load_root("my_drive")
91175

@@ -212,10 +296,12 @@ def on_nav_clicked(self, item: QListWidgetItem):
212296

213297
def populate_files(self, items: List[Dict[str, Any]]):
214298
self.file_view.clear()
299+
self.item_mapping.clear()
215300

216301
# Sort folders first, then files, both alphabetically
217302
folders = []
218303
files = []
304+
items_needing_thumbnails = []
219305
for item in items:
220306
mime = item.get('mimeType', '')
221307
if mime == 'application/vnd.google-apps.folder':
@@ -229,11 +315,33 @@ def populate_files(self, items: List[Dict[str, Any]]):
229315
for item in folders + files:
230316
name = item.get('name', 'Unknown')
231317
mime = item.get('mimeType', '')
318+
file_id = item.get('id', '')
232319

233320
list_item = QListWidgetItem(name)
234-
list_item.setIcon(get_mime_icon(mime))
321+
322+
cached_pixmap = self.thumbnail_cache.get(file_id)
323+
if cached_pixmap:
324+
list_item.setIcon(QIcon(cached_pixmap))
325+
else:
326+
list_item.setIcon(get_mime_icon(mime))
327+
if mime == 'application/pdf':
328+
items_needing_thumbnails.append(item)
329+
235330
list_item.setData(Qt.UserRole, item)
236331
self.file_view.addItem(list_item)
332+
self.item_mapping[file_id] = list_item
333+
334+
if items_needing_thumbnails:
335+
kickoff_worker = ThumbnailKickoffWorker(items_needing_thumbnails, self.on_thumbnail_loaded)
336+
self.threadpool.start(kickoff_worker)
337+
338+
def on_thumbnail_loaded(self, file_id: str, img: QImage):
339+
pixmap = QPixmap.fromImage(img)
340+
self.thumbnail_cache.put(file_id, pixmap)
341+
if file_id in self.item_mapping:
342+
item = self.item_mapping[file_id]
343+
if item.listWidget() == self.file_view:
344+
item.setIcon(QIcon(pixmap))
237345

238346
def on_item_activated(self, item: QListWidgetItem):
239347
file_data = item.data(Qt.UserRole)

‎scripts/gdrive_base.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def get_file_contents(fileid, verbose=True):
217217
return buffer
218218

219219
def fetch_preview_image(fileid: str, size=1000) -> bytes | None:
220-
"""Fetches the thumbnail image for a file and returns it in a BytesIO buffer"""
220+
"""Fetches the thumbnail image for a file and returns it as raw bytes"""
221221
try:
222222
file_metadata = execute(session().files().get(fileId=fileid, fields="thumbnailLink"))
223223
thumbnail_url = file_metadata.get('thumbnailLink')

‎scripts/local_gdrive.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,17 @@ def _move_to_trash(self, file_id: str, trashed_time: str = None):
828828
self.cursor.execute("UPDATE trashed_drive_items SET trashed_time = ? WHERE id = ?", (trashed_time, file_id))
829829
return
830830

831-
self.cursor.execute("INSERT INTO trashed_drive_items SELECT * FROM drive_items WHERE id = ?", (file_id,))
831+
self.cursor.execute("""
832+
INSERT INTO trashed_drive_items (
833+
id, version, name, original_name, mime_type, parent_id,
834+
modified_time, size, owner, md5_checksum, shortcut_target
835+
)
836+
SELECT
837+
id, version, name, original_name, mime_type, parent_id,
838+
modified_time, size, owner, md5_checksum, shortcut_target
839+
FROM drive_items
840+
WHERE id = ?
841+
""", (file_id,))
832842
self.cursor.execute("DELETE FROM drive_items WHERE id = ?", (file_id,))
833843

834844
if trashed_time:

‎scripts/pdfutils.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ def render_pdf_thumbnail(path, min_d_size=256, max_d_size=512, type='png') -> by
3636
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
3737
return pix.tobytes(type)
3838

39-
def get_shared_cached_pdf_thumbnail(pdf_path: Path, size='large') -> bytes:
39+
def get_cached_pdf_thumbnail(pdf_path: Path, size='large') -> bytes:
4040
thumbnail_path = thumbnail_path_for_file(pdf_path, shared=True, size=size)
4141
if thumbnail_path.is_file():
4242
return thumbnail_path.read_bytes()
43+
alt_path = thumbnail_path_for_file(pdf_path, shared=False, size=size)
44+
if alt_path.is_file():
45+
return alt_path.read_bytes()
4346
tsize = THUMBNAIL_SIZES[size]
4447
thebytes = render_pdf_thumbnail(
4548
pdf_path,

0 commit comments

Comments
 (0)