mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-31 15:19:10 +00:00
fix: ui/ux parity fixes for thumbnails and files (#608)
* fix(ui): display loading icon before rendered thumb * fix: skip out of range thumbs * fix: optimize library refreshing * fix(ui): tag colors show correct names * fix(ui): ensure inner field containers are deleted * fix(ui): don't show default preview label text * fix: catch all missing file thumbs; clean up logs
This commit is contained in:
committed by
GitHub
parent
d152cd75d8
commit
1fb1a80d53
6
tagstudio/src/core/exceptions.py
Normal file
6
tagstudio/src/core/exceptions.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
class NoRendererError(Exception): ...
|
||||
@@ -131,6 +131,7 @@ class Library:
|
||||
storage_path: Path | str | None
|
||||
engine: Engine | None
|
||||
folder: Folder | None
|
||||
included_files: set[Path] = set()
|
||||
|
||||
FILENAME: str = "ts_library.sqlite"
|
||||
|
||||
@@ -140,6 +141,7 @@ class Library:
|
||||
self.library_dir = None
|
||||
self.storage_path = None
|
||||
self.folder = None
|
||||
self.included_files = set()
|
||||
|
||||
def open_library(self, library_dir: Path, storage_path: str | None = None) -> LibraryStatus:
|
||||
if storage_path == ":memory:":
|
||||
|
||||
@@ -9,6 +9,19 @@ from src.core.library import Entry, Library
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
GLOBAL_IGNORE_SET: set[str] = set(
|
||||
[
|
||||
TS_FOLDER_NAME,
|
||||
"$RECYCLE.BIN",
|
||||
".Trashes",
|
||||
".Trash",
|
||||
"tagstudio_thumbs",
|
||||
".fseventsd",
|
||||
".Spotlight-V100",
|
||||
"System Volume Information",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefreshDirTracker:
|
||||
@@ -49,29 +62,45 @@ class RefreshDirTracker:
|
||||
self.files_not_in_library = []
|
||||
dir_file_count = 0
|
||||
|
||||
for path in lib_path.glob("**/*"):
|
||||
str_path = str(path)
|
||||
if path.is_dir():
|
||||
for f in lib_path.glob("**/*"):
|
||||
end_time_loop = time()
|
||||
# Yield output every 1/30 of a second
|
||||
if (end_time_loop - start_time_loop) > 0.034:
|
||||
yield dir_file_count
|
||||
start_time_loop = time()
|
||||
|
||||
# Skip if the file/path is already mapped in the Library
|
||||
if f in self.library.included_files:
|
||||
dir_file_count += 1
|
||||
continue
|
||||
|
||||
if "$RECYCLE.BIN" in str_path or TS_FOLDER_NAME in str_path:
|
||||
# Ignore if the file is a directory
|
||||
if f.is_dir():
|
||||
continue
|
||||
|
||||
# Ensure new file isn't in a globally ignored folder
|
||||
skip: bool = False
|
||||
for part in f.parts:
|
||||
if part in GLOBAL_IGNORE_SET:
|
||||
skip = True
|
||||
break
|
||||
if skip:
|
||||
continue
|
||||
|
||||
dir_file_count += 1
|
||||
relative_path = path.relative_to(lib_path)
|
||||
self.library.included_files.add(f)
|
||||
|
||||
relative_path = f.relative_to(lib_path)
|
||||
# TODO - load these in batch somehow
|
||||
if not self.library.has_path_entry(relative_path):
|
||||
self.files_not_in_library.append(relative_path)
|
||||
|
||||
# Yield output every 1/30 of a second
|
||||
if (time() - start_time_loop) > 0.034:
|
||||
yield dir_file_count
|
||||
start_time_loop = time()
|
||||
|
||||
end_time_total = time()
|
||||
yield dir_file_count
|
||||
logger.info(
|
||||
"Directory scan time",
|
||||
path=lib_path,
|
||||
duration=(end_time_total - start_time_total),
|
||||
new_files_count=dir_file_count,
|
||||
files_not_in_lib=self.files_not_in_library,
|
||||
files_scanned=dir_file_count,
|
||||
)
|
||||
|
||||
@@ -19,9 +19,9 @@ def open_file(path: str | Path, file_manager: bool = False):
|
||||
"""Open a file in the default application or file explorer.
|
||||
|
||||
Args:
|
||||
path (str): The path to the file to open.
|
||||
file_manager (bool, optional): Whether to open the file in the file manager
|
||||
(e.g. Finder on macOS). Defaults to False.
|
||||
path (str): The path to the file to open.
|
||||
file_manager (bool, optional): Whether to open the file in the file manager
|
||||
(e.g. Finder on macOS). Defaults to False.
|
||||
"""
|
||||
path = Path(path)
|
||||
logger.info("Opening file", path=path)
|
||||
@@ -93,7 +93,7 @@ class FileOpenerHelper:
|
||||
"""Initialize the FileOpenerHelper.
|
||||
|
||||
Args:
|
||||
filepath (str): The path to the file to open.
|
||||
filepath (str): The path to the file to open.
|
||||
"""
|
||||
self.filepath = str(filepath)
|
||||
|
||||
@@ -101,7 +101,7 @@ class FileOpenerHelper:
|
||||
"""Set the filepath to open.
|
||||
|
||||
Args:
|
||||
filepath (str): The path to the file to open.
|
||||
filepath (str): The path to the file to open.
|
||||
"""
|
||||
self.filepath = str(filepath)
|
||||
|
||||
@@ -115,20 +115,19 @@ class FileOpenerHelper:
|
||||
|
||||
|
||||
class FileOpenerLabel(QLabel):
|
||||
def __init__(self, text, parent=None):
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize the FileOpenerLabel.
|
||||
|
||||
Args:
|
||||
text (str): The text to display.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(text, parent)
|
||||
super().__init__(parent)
|
||||
|
||||
def set_file_path(self, filepath):
|
||||
"""Set the filepath to open.
|
||||
|
||||
Args:
|
||||
filepath (str): The path to the file to open.
|
||||
filepath (str): The path to the file to open.
|
||||
"""
|
||||
self.filepath = filepath
|
||||
|
||||
@@ -139,7 +138,7 @@ class FileOpenerLabel(QLabel):
|
||||
On a right click, show a context menu.
|
||||
|
||||
Args:
|
||||
event (QMouseEvent): The mouse press event.
|
||||
event (QMouseEvent): The mouse press event.
|
||||
"""
|
||||
super().mousePressEvent(event)
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ class BuildTagPanel(PanelWidget):
|
||||
self.color_field.setMaxVisibleItems(10)
|
||||
self.color_field.setStyleSheet("combobox-popup:0;")
|
||||
for color in TagColor:
|
||||
self.color_field.addItem(color.name, userData=color.value)
|
||||
self.color_field.addItem(color.name.replace("_", " ").title(), userData=color.value)
|
||||
# self.color_field.setProperty("appearance", "flat")
|
||||
self.color_field.currentIndexChanged.connect(
|
||||
lambda c: (
|
||||
|
||||
@@ -1031,25 +1031,41 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.flow_container.layout().update()
|
||||
self.main_window.update()
|
||||
|
||||
for idx, (entry, item_thumb) in enumerate(
|
||||
zip_longest(self.frame_content, self.item_thumbs)
|
||||
):
|
||||
is_grid_thumb = True
|
||||
# Show loading placeholder icons
|
||||
for entry, item_thumb in zip_longest(self.frame_content, self.item_thumbs):
|
||||
if not entry:
|
||||
item_thumb.hide()
|
||||
continue
|
||||
|
||||
filepath = self.lib.library_dir / entry.path
|
||||
item_thumb = self.item_thumbs[idx]
|
||||
item_thumb.set_mode(ItemType.ENTRY)
|
||||
item_thumb.set_item_id(entry)
|
||||
|
||||
# TODO - show after item is rendered
|
||||
item_thumb.show()
|
||||
|
||||
is_loading = True
|
||||
self.thumb_job_queue.put(
|
||||
(
|
||||
item_thumb.renderer.render,
|
||||
(sys.float_info.max, "", base_size, ratio, True, True),
|
||||
(sys.float_info.max, "", base_size, ratio, is_loading, is_grid_thumb),
|
||||
)
|
||||
)
|
||||
|
||||
# Show rendered thumbnails
|
||||
for idx, (entry, item_thumb) in enumerate(
|
||||
zip_longest(self.frame_content, self.item_thumbs)
|
||||
):
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
filepath = self.lib.library_dir / entry.path
|
||||
is_loading = False
|
||||
|
||||
self.thumb_job_queue.put(
|
||||
(
|
||||
item_thumb.renderer.render,
|
||||
(time.time(), filepath, base_size, ratio, is_loading, is_grid_thumb),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1188,7 +1204,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.filter.page_size = self.lib.prefs(LibraryPrefs.PAGE_SIZE)
|
||||
|
||||
# TODO - make this call optional
|
||||
self.add_new_files_callback()
|
||||
if self.lib.entries_count < 10000:
|
||||
self.add_new_files_callback()
|
||||
|
||||
self.update_libs_list(path)
|
||||
title_text = f"{self.base_title} - Library '{self.lib.library_dir}'"
|
||||
|
||||
@@ -135,7 +135,10 @@ class FieldContainer(QWidget):
|
||||
|
||||
def set_inner_widget(self, widget: "FieldWidget"):
|
||||
if self.field_layout.itemAt(0):
|
||||
self.field_layout.itemAt(0).widget().deleteLater()
|
||||
old: QWidget = self.field_layout.itemAt(0).widget()
|
||||
self.field_layout.removeWidget(old)
|
||||
old.deleteLater()
|
||||
|
||||
self.field_layout.addWidget(widget)
|
||||
|
||||
def get_inner_widget(self):
|
||||
|
||||
@@ -162,23 +162,27 @@ class PreviewPanel(QWidget):
|
||||
image_layout.addWidget(self.preview_vid)
|
||||
image_layout.setAlignment(self.preview_vid, Qt.AlignmentFlag.AlignCenter)
|
||||
self.image_container.setMinimumSize(*self.img_button_size)
|
||||
self.file_label = FileOpenerLabel("filename")
|
||||
self.file_label = FileOpenerLabel()
|
||||
self.file_label.setObjectName("filenameLabel")
|
||||
self.file_label.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.file_label.setWordWrap(True)
|
||||
self.file_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
self.file_label.setStyleSheet(file_label_style)
|
||||
|
||||
self.date_created_label = QLabel("dateCreatedLabel")
|
||||
self.date_created_label = QLabel()
|
||||
self.date_created_label.setObjectName("dateCreatedLabel")
|
||||
self.date_created_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.date_created_label.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.date_created_label.setStyleSheet(date_style)
|
||||
|
||||
self.date_modified_label = QLabel("dateModifiedLabel")
|
||||
self.date_modified_label = QLabel()
|
||||
self.date_modified_label.setObjectName("dateModifiedLabel")
|
||||
self.date_modified_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.date_modified_label.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.date_modified_label.setStyleSheet(date_style)
|
||||
|
||||
self.dimensions_label = QLabel("dimensionsLabel")
|
||||
self.dimensions_label = QLabel()
|
||||
self.dimensions_label.setObjectName("dimensionsLabel")
|
||||
self.dimensions_label.setWordWrap(True)
|
||||
self.dimensions_label.setStyleSheet(properties_style)
|
||||
|
||||
@@ -480,7 +484,7 @@ class PreviewPanel(QWidget):
|
||||
if filepath and filepath.is_file():
|
||||
created: dt = None
|
||||
if platform.system() == "Windows" or platform.system() == "Darwin":
|
||||
created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined]
|
||||
created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined, unused-ignore]
|
||||
else:
|
||||
created = dt.fromtimestamp(filepath.stat().st_ctime)
|
||||
modified: dt = dt.fromtimestamp(filepath.stat().st_mtime)
|
||||
|
||||
@@ -45,6 +45,7 @@ from PySide6.QtGui import QGuiApplication, QImage, QPainter, QPixmap
|
||||
from PySide6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
from src.core.constants import FONT_SAMPLE_SIZES, FONT_SAMPLE_TEXT
|
||||
from src.core.exceptions import NoRendererError
|
||||
from src.core.media_types import MediaCategories, MediaType
|
||||
from src.core.palette import ColorType, UiColor, get_ui_color
|
||||
from src.core.utils.encoding import detect_char_encoding
|
||||
@@ -470,7 +471,7 @@ class ThumbRenderer(QObject):
|
||||
id3.ID3NoHeaderError,
|
||||
MutagenError,
|
||||
) as e:
|
||||
logger.error("Couldn't read album artwork", path=filepath, error=e)
|
||||
logger.error("Couldn't read album artwork", path=filepath, error=type(e).__name__)
|
||||
return image
|
||||
|
||||
def _audio_waveform_thumb(
|
||||
@@ -555,7 +556,7 @@ class ThumbRenderer(QObject):
|
||||
im.resize((size, size), Image.Resampling.BILINEAR)
|
||||
|
||||
except exceptions.CouldntDecodeError as e:
|
||||
logger.error("Couldn't render waveform", path=filepath.name, error=e)
|
||||
logger.error("Couldn't render waveform", path=filepath.name, error=type(e).__name__)
|
||||
|
||||
return im
|
||||
|
||||
@@ -581,7 +582,6 @@ class ThumbRenderer(QObject):
|
||||
except (
|
||||
AttributeError,
|
||||
UnidentifiedImageError,
|
||||
FileNotFoundError,
|
||||
TypeError,
|
||||
) as e:
|
||||
if str(e) == "expected string or buffer":
|
||||
@@ -591,7 +591,7 @@ class ThumbRenderer(QObject):
|
||||
)
|
||||
|
||||
else:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _source_engine(self, filepath: Path) -> Image.Image:
|
||||
@@ -607,15 +607,14 @@ class ThumbRenderer(QObject):
|
||||
except (
|
||||
AttributeError,
|
||||
UnidentifiedImageError,
|
||||
FileNotFoundError,
|
||||
TypeError,
|
||||
struct.error,
|
||||
) as e:
|
||||
if str(e) == "expected string or buffer":
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
|
||||
else:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
@classmethod
|
||||
@@ -661,7 +660,7 @@ class ThumbRenderer(QObject):
|
||||
image_data = zip_file.read(file_name)
|
||||
im = Image.open(BytesIO(image_data))
|
||||
except Exception as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
|
||||
return im
|
||||
|
||||
@@ -722,7 +721,7 @@ class ThumbRenderer(QObject):
|
||||
)
|
||||
im = self._apply_overlay_color(bg, UiColor.PURPLE)
|
||||
except OSError as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _font_long_thumb(self, filepath: Path, size: int) -> Image.Image:
|
||||
@@ -753,7 +752,7 @@ class ThumbRenderer(QObject):
|
||||
)[-1]
|
||||
im = theme_fg_overlay(bg, use_alpha=False)
|
||||
except OSError as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _image_raw_thumb(self, filepath: Path) -> Image.Image:
|
||||
@@ -772,13 +771,12 @@ class ThumbRenderer(QObject):
|
||||
rgb,
|
||||
decoder_name="raw",
|
||||
)
|
||||
except DecompressionBombError as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
except (
|
||||
DecompressionBombError,
|
||||
rawpy._rawpy.LibRawIOError,
|
||||
rawpy._rawpy.LibRawFileUnsupportedError,
|
||||
) as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _image_thumb(self, filepath: Path) -> Image.Image:
|
||||
@@ -796,13 +794,12 @@ class ThumbRenderer(QObject):
|
||||
new_bg = Image.new("RGB", im.size, color="#1e1e1e")
|
||||
new_bg.paste(im, mask=im.getchannel(3))
|
||||
im = new_bg
|
||||
|
||||
im = ImageOps.exif_transpose(im)
|
||||
except (
|
||||
UnidentifiedImageError,
|
||||
DecompressionBombError,
|
||||
) as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
@classmethod
|
||||
@@ -955,7 +952,7 @@ class ThumbRenderer(QObject):
|
||||
UnicodeDecodeError,
|
||||
OSError,
|
||||
) as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _video_thumb(self, filepath: Path) -> Image.Image:
|
||||
@@ -996,7 +993,7 @@ class ThumbRenderer(QObject):
|
||||
DecompressionBombError,
|
||||
OSError,
|
||||
) as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def render(
|
||||
@@ -1040,6 +1037,28 @@ class ThumbRenderer(QObject):
|
||||
"thumb_loading", theme_color, (adj_size, adj_size), pixel_ratio
|
||||
)
|
||||
|
||||
def render_default() -> Image.Image:
|
||||
if update_on_ratio_change:
|
||||
self.updated_ratio.emit(1)
|
||||
im = self._get_icon(
|
||||
name=self._get_resource_id(_filepath),
|
||||
color=theme_color,
|
||||
size=(adj_size, adj_size),
|
||||
pixel_ratio=pixel_ratio,
|
||||
)
|
||||
return im
|
||||
|
||||
def render_unlinked() -> Image.Image:
|
||||
if update_on_ratio_change:
|
||||
self.updated_ratio.emit(1)
|
||||
im = self._get_icon(
|
||||
name="broken_link_icon",
|
||||
color=UiColor.RED,
|
||||
size=(adj_size, adj_size),
|
||||
pixel_ratio=pixel_ratio,
|
||||
)
|
||||
return im
|
||||
|
||||
if is_loading:
|
||||
final = loading_thumb.resize((adj_size, adj_size), resample=Image.Resampling.BILINEAR)
|
||||
qim = ImageQt.ImageQt(final)
|
||||
@@ -1049,6 +1068,9 @@ class ThumbRenderer(QObject):
|
||||
self.updated_ratio.emit(1)
|
||||
elif _filepath:
|
||||
try:
|
||||
# Missing Files ================================================
|
||||
if not _filepath.exists():
|
||||
raise FileNotFoundError
|
||||
ext: str = _filepath.suffix.lower()
|
||||
# Images =======================================================
|
||||
if MediaCategories.is_ext_in_category(
|
||||
@@ -1122,10 +1144,8 @@ class ThumbRenderer(QObject):
|
||||
):
|
||||
image = self._source_engine(_filepath)
|
||||
# No Rendered Thumbnail ========================================
|
||||
if not _filepath.exists():
|
||||
raise FileNotFoundError
|
||||
elif not image:
|
||||
raise UnidentifiedImageError
|
||||
if not image:
|
||||
raise NoRendererError
|
||||
|
||||
orig_x, orig_y = image.size
|
||||
new_x, new_y = (adj_size, adj_size)
|
||||
@@ -1161,32 +1181,19 @@ class ThumbRenderer(QObject):
|
||||
final = Image.new("RGBA", image.size, (0, 0, 0, 0))
|
||||
final.paste(image, mask=mask.getchannel(0))
|
||||
|
||||
except FileNotFoundError as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
if update_on_ratio_change:
|
||||
self.updated_ratio.emit(1)
|
||||
final = self._get_icon(
|
||||
name="broken_link_icon",
|
||||
color=UiColor.RED,
|
||||
size=(adj_size, adj_size),
|
||||
pixel_ratio=pixel_ratio,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
final = render_unlinked()
|
||||
except (
|
||||
UnidentifiedImageError,
|
||||
DecompressionBombError,
|
||||
ValueError,
|
||||
ChildProcessError,
|
||||
) as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
final = render_default()
|
||||
except NoRendererError:
|
||||
final = render_default()
|
||||
|
||||
if update_on_ratio_change:
|
||||
self.updated_ratio.emit(1)
|
||||
final = self._get_icon(
|
||||
name=self._get_resource_id(_filepath),
|
||||
color=theme_color,
|
||||
size=(adj_size, adj_size),
|
||||
pixel_ratio=pixel_ratio,
|
||||
)
|
||||
qim = ImageQt.ImageQt(final)
|
||||
if image:
|
||||
image.close()
|
||||
|
||||
@@ -15,10 +15,11 @@ def test_refresh_new_files(library, exclude_mode):
|
||||
library.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, exclude_mode)
|
||||
library.set_prefs(LibraryPrefs.EXTENSION_LIST, [".md"])
|
||||
registry = RefreshDirTracker(library=library)
|
||||
library.included_files.clear()
|
||||
(library.library_dir / "FOO.MD").touch()
|
||||
|
||||
# When
|
||||
assert not list(registry.refresh_dir(library.library_dir))
|
||||
assert len(list(registry.refresh_dir(library.library_dir))) == 1
|
||||
|
||||
# Then
|
||||
assert registry.files_not_in_library == [pathlib.Path("FOO.MD")]
|
||||
|
||||
Reference in New Issue
Block a user