Skip to content

Commit 3bcad8f

Browse files
committed
feat: take Ignore List into consideration when refreshing directory
1 parent af642a7 commit 3bcad8f

File tree

8 files changed

+74
-49
lines changed

8 files changed

+74
-49
lines changed

.github/workflows/mypy.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ jobs:
2323

2424
- name: Install dependencies
2525
run: |
26-
pip install -r requirements.txt
27-
pip install mypy==1.11.2
26+
python -m pip install --upgrade uv
27+
uv pip install -r requirements.txt
28+
uv pip install mypy==1.11.2
2829
mkdir tagstudio/.mypy_cache
2930
3031
- uses: tsuyoshicho/action-mypy@v4

tagstudio/src/core/library/alchemy/library.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
selectinload,
2626
make_transient,
2727
)
28-
from typing import TYPE_CHECKING
2928

3029
from .db import make_tables
3130
from .enums import TagColor, FilterState, FieldTypeEnum
@@ -46,10 +45,6 @@
4645
BACKUP_FOLDER_NAME,
4746
)
4847

49-
if TYPE_CHECKING:
50-
from ...utils.dupe_files import DupeRegistry
51-
from ...utils.missing_files import MissingRegistry
52-
5348
LIBRARY_FILENAME: str = "ts_library.sqlite"
5449

5550
logger = structlog.get_logger(__name__)
@@ -100,11 +95,6 @@ class Library:
10095
engine: Engine | None
10196
folder: Folder | None
10297

103-
ignored_extensions: list[str]
104-
105-
missing_tracker: "MissingRegistry"
106-
dupe_tracker: "DupeRegistry"
107-
10898
def close(self):
10999
if self.engine:
110100
self.engine.dispose()
@@ -182,9 +172,6 @@ def open_library(
182172
session.commit()
183173
self.folder = folder
184174

185-
# load ignored extensions
186-
self.ignored_extensions = self.prefs(LibraryPrefs.EXTENSION_LIST)
187-
188175
@property
189176
def default_fields(self) -> list[BaseField]:
190177
with Session(self.engine) as session:
Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import time
1+
from time import time
22
from collections.abc import Iterator
33
from dataclasses import dataclass, field
44
from pathlib import Path
55

6-
from src.core.constants import TS_FOLDER_NAME
6+
import structlog
7+
8+
from src.core.constants import TS_FOLDER_NAME, LibraryPrefs
79
from src.core.library import Library, Entry
810

11+
logger = structlog.get_logger(__name__)
12+
913

1014
@dataclass
1115
class RefreshDirTracker:
1216
library: Library
13-
dir_file_count: int = 0
1417
files_not_in_library: list[Path] = field(default_factory=list)
1518

1619
@property
@@ -36,38 +39,57 @@ def save_new_files(self) -> Iterator[int]:
3639

3740
self.files_not_in_library = []
3841

39-
def refresh_dir(self) -> Iterator[int]:
42+
def refresh_dir(self, lib_path: Path) -> Iterator[int]:
4043
"""Scan a directory for files, and add those relative filenames to internal variables."""
41-
if self.library.folder is None:
42-
raise ValueError("No folder set.")
44+
if self.library.library_dir is None:
45+
raise ValueError("No library directory set.")
46+
47+
is_exclude_list = self.library.prefs(LibraryPrefs.IS_EXCLUDE_LIST)
48+
exclude_list = set(self.library.prefs(LibraryPrefs.EXTENSION_LIST))
49+
50+
def skip_suffix(suffix: str) -> bool:
51+
"""Determine if the file extension should be skipped.
52+
53+
Declared as local function as it's faster.
54+
55+
- check if the suffix is in the library's "exclude list"
56+
- if library uses "exclude mode", and extensions is in the list, we skip
57+
- if library uses "include mode", and extensions is not in the list, we skip
58+
"""
59+
return (suffix.lower() in exclude_list) == is_exclude_list
60+
61+
start_time_total = time()
62+
start_time_loop = time()
4363

44-
start_time = time.time()
4564
self.files_not_in_library = []
46-
self.dir_file_count = 0
47-
48-
lib_path = self.library.folder.path
49-
50-
for path in lib_path.glob("**/*"):
51-
str_path = str(path)
52-
if (
53-
path.is_dir()
54-
or "$RECYCLE.BIN" in str_path
55-
or TS_FOLDER_NAME in str_path
56-
or "tagstudio_thumbs" in str_path
57-
):
65+
dir_file_count = 0
66+
67+
for path_item in lib_path.glob("**/*"):
68+
str_path = str(path_item)
69+
if path_item.is_dir():
5870
continue
5971

60-
suffix = path.suffix.lower().lstrip(".")
61-
if suffix in self.library.ignored_extensions:
72+
if "$RECYCLE.BIN" in str_path or TS_FOLDER_NAME in str_path:
6273
continue
6374

64-
self.dir_file_count += 1
65-
relative_path = path.relative_to(lib_path)
75+
if skip_suffix(path_item.suffix):
76+
continue
77+
78+
dir_file_count += 1
79+
relative_path = path_item.relative_to(lib_path)
6680
# TODO - load these in batch somehow
6781
if not self.library.has_path_entry(relative_path):
6882
self.files_not_in_library.append(relative_path)
6983

70-
end_time = time.time()
7184
# Yield output every 1/30 of a second
72-
if (end_time - start_time) > 0.034:
73-
yield self.dir_file_count
85+
if (time() - start_time_loop) > 0.034:
86+
yield dir_file_count
87+
start_time_loop = time()
88+
89+
end_time_total = time()
90+
logger.info(
91+
"Directory scan time",
92+
path=lib_path,
93+
duration=(end_time_total - start_time_total),
94+
new_files_count=dir_file_count,
95+
)

tagstudio/src/qt/ts_qt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ def add_new_files_callback(self):
675675
)
676676
pw.show()
677677

678-
iterator = FunctionIterator(tracker.refresh_dir)
678+
iterator = FunctionIterator(lambda: tracker.refresh_dir(self.lib.library_dir))
679679
iterator.value.connect(
680680
lambda x: (
681681
pw.update_progress(x + 1),

tagstudio/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def cwd():
2424
@pytest.fixture
2525
def library(request):
2626
# when no param is passed, use the default
27-
library_path = "/tmp/"
27+
library_path = "/dev/null/"
2828
if hasattr(request, "param"):
2929
if isinstance(request.param, TemporaryDirectory):
3030
library_path = request.param.name

tagstudio/tests/macros/test_dupe_entries.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
def test_refresh_dupe_files(library):
10+
library.library_dir = "/tmp/"
1011
entry = Entry(
1112
folder=library.folder,
1213
path=pathlib.Path("bar/foo.txt"),

tagstudio/tests/macros/test_refresh_dir.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,29 @@
22
from tempfile import TemporaryDirectory
33

44
import pytest
5+
6+
from src.core.constants import LibraryPrefs
57
from src.core.utils.refresh_dir import RefreshDirTracker
68

79
CWD = pathlib.Path(__file__).parent
810

911

12+
@pytest.mark.parametrize("exclude_mode", [True, False])
1013
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
11-
def test_refresh_new_files(library):
14+
def test_refresh_new_files(library, exclude_mode):
15+
# Given
16+
library.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, exclude_mode)
17+
library.set_prefs(LibraryPrefs.EXTENSION_LIST, [".md"])
1218
registry = RefreshDirTracker(library=library)
19+
(library.library_dir / "FOO.MD").touch()
1320

14-
# touch new files to simulate new files
15-
(library.library_dir / "foo.md").touch()
16-
17-
assert not list(registry.refresh_dir())
21+
# When
22+
assert not list(registry.refresh_dir(library.library_dir))
1823

19-
assert registry.files_not_in_library == [pathlib.Path("foo.md")]
24+
# Then
25+
if exclude_mode:
26+
# .md is in the list & is_exclude_list is True - should not be registered
27+
assert not registry.files_not_in_library
28+
else:
29+
# .md is in the list & is_exclude_list is False - should be registered
30+
assert registry.files_not_in_library == [pathlib.Path("FOO.MD")]

tagstudio/tests/qt/test_preview_panel.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pathlib import Path
2+
from tempfile import TemporaryDirectory
23

4+
import pytest
35

46
from src.core.library import Entry
57
from src.core.library.alchemy.enums import FieldTypeEnum
@@ -18,6 +20,7 @@ def test_update_widgets_not_selected(qt_driver, library):
1820
assert panel.file_label.text() == "No Items Selected"
1921

2022

23+
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
2124
def test_update_widgets_single_selected(qt_driver, library):
2225
qt_driver.frame_content = list(library.get_entries())
2326
qt_driver.selected = [0]

0 commit comments

Comments
 (0)