Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic detection of filesystem changes #125

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,6 @@ compile_commands.json

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets

# Local History for Visual Studio Code
.history/
Expand Down
17 changes: 0 additions & 17 deletions .vscode/launch.json

This file was deleted.

2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ typing_extensions>=3.10.0.0,<=4.11.0
ujson>=5.8.0,<=5.9.0
rawpy==0.21.0
pillow-heif==0.16.0
watchdog>=4.0.0,<5.0.0
cachetools>=5.3.3,<6.0.0
55 changes: 55 additions & 0 deletions tagstudio/src/core/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import os
import sys
import time
from typing import Callable
import traceback
import typing
import xml.etree.ElementTree as ET
Expand All @@ -20,6 +21,9 @@
from typing_extensions import Self

import ujson
from cachetools import cached, LRUCache
from watchdog.observers import Observer
from watchdog.events import FileSystemEvent, DirModifiedEvent, FileSystemEventHandler

from src.core.json_typing import JsonCollation, JsonEntry, JsonLibary, JsonTag
from src.core.utils.str import strip_punctuation
Expand Down Expand Up @@ -323,6 +327,44 @@ def compressed_dict(self) -> JsonCollation:
return obj


class IsDirModifiedHandler(FileSystemEventHandler):
def __init__(self, lib: "Library", callback: Callable[[], None]) -> None:
self.library: Library = lib
self.callback: Callable[[], None] = callback
super().__init__()

@cached(cache=LRUCache(maxsize=32), key=lambda _, path, time: (path, time))
def event_helper(self, filepath: str, time: int) -> None:
_ = time
# Here update internal vars with single new file
if "$RECYCLE.BIN" in filepath or "tagstudio_thumbs" in filepath:
return

if (
os.path.splitext(filepath)[1][1:].lower()
not in self.library.ignored_extensions
):
file = os.path.relpath(filepath, self.library.library_dir)

if os.name == "nt":
id = self.library.filename_to_entry_id_map.get(file.lower())
else:
id = self.library.filename_to_entry_id_map.get(file)

if id is None:
self.library.dir_file_count += 1
self.library.files_not_in_library.append(file)
self.library.add_new_files_as_entries()

self.callback()

def on_any_event(self, event: FileSystemEvent) -> None:
filepath = event.dest_path if event.dest_path else event.src_path
now = round(time.time())
if not event.is_directory:
self.event_helper(filepath, now)


class Library:
"""Class for the Library object, and all CRUD operations made upon it."""

Expand Down Expand Up @@ -439,6 +481,10 @@ def __init__(self) -> None:
{"id": 30, "name": "Comments", "type": "text_box"},
]

# Refresh on changes
self.observer = Observer()
self.observer.start()

def create_library(self, path) -> int:
"""
Creates a TagStudio library in the given directory.\n
Expand Down Expand Up @@ -888,6 +934,9 @@ def clear_internal_vars(self):
self._tag_id_to_index_map = {}
self._tag_entry_ref_map.clear()

if self.observer.is_alive():
self.observer.unschedule_all()

def refresh_dir(self) -> Generator:
"""Scans a directory for files, and adds those relative filenames to internal variables."""

Expand Down Expand Up @@ -950,6 +999,12 @@ def refresh_dir(self) -> Generator:
f"[LIBRARY][INFO] Not bothering to sort files because there's OVER 100,000! Better sorting methods will be added in the future."
)

def refresh_on_changes(self, callback: Callable[[], None]) -> None:
"""Activates the automatic refreshing of the Library on filesystem modifications"""
self.observer.schedule(
IsDirModifiedHandler(self, callback), path=self.library_dir, recursive=True
)

def refresh_missing_files(self):
"""Tracks the number of Entries that point to an invalid file path."""
self.missing_files.clear()
Expand Down
38 changes: 17 additions & 21 deletions tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ def handleSIGTERM(self):

def shutdown(self):
"""Save Library on Application Exit"""
self.lib.observer.stop()
if self.lib.library_dir:
self.save_library()
self.settings.setValue(SettingItems.LAST_LIBRARY, self.lib.library_dir)
Expand Down Expand Up @@ -1215,14 +1216,14 @@ def update_thumbs(self):
lambda checked=False, entry=entry: self.select_item(
ItemType.ENTRY,
entry.id,
append=True
if QGuiApplication.keyboardModifiers()
== Qt.KeyboardModifier.ControlModifier
else False,
bridge=True
if QGuiApplication.keyboardModifiers()
== Qt.KeyboardModifier.ShiftModifier
else False,
append=(
QGuiApplication.keyboardModifiers()
== Qt.KeyboardModifier.ControlModifier
),
bridge=(
QGuiApplication.keyboardModifiers()
== Qt.KeyboardModifier.ShiftModifier
),
)
)
)
Expand Down Expand Up @@ -1422,6 +1423,7 @@ def open_library(self, path):
self.selected.clear()
self.preview_panel.update_widgets()
self.filter_items()
self.lib.refresh_on_changes(self.filter_items)

def create_collage(self) -> None:
"""Generates and saves an image collage based on Library Entries."""
Expand Down Expand Up @@ -1476,19 +1478,13 @@ def create_collage(self) -> None:
# ], prompt='', required=True)
full_thumb_size = 0

thumb_size: int = (
32
if (full_thumb_size == 0)
else 64
if (full_thumb_size == 1)
else 128
if (full_thumb_size == 2)
else 256
if (full_thumb_size == 3)
else 512
if (full_thumb_size == 4)
else 32
)
size_options = {
1: 64,
2: 128,
3: 256,
4: 512,
}
thumb_size = size_options.get(full_thumb_size, 32)
thumb_size = 16

# if len(com) > 1 and com[1] == 'keep-aspect':
Expand Down