From 97e0e80f6f48adef5be74d73f4810d3b0db6b335 Mon Sep 17 00:00:00 2001 From: python357-1 <30739625+python357-1@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:52:00 -0600 Subject: [PATCH] feat: add filetype and mediatype searches (#575) * feat: add filetype and mediatype searches * style: fix some style issues * fix: parametrize mediatype and filetype tests * style: fix remaining unordered import * style: fix pytest parametrize calls * feat: add human-readable names to mediacategories * feat: use human-readable names in mediacategory: search * feat: add human-readable name to open document * fix: fix returning multiple filetypes issue and add regression test --- tagstudio/src/core/library/alchemy/enums.py | 8 +++++ tagstudio/src/core/library/alchemy/library.py | 13 ++++++++ tagstudio/src/core/media_types.py | 31 +++++++++++++++++++ tagstudio/tests/conftest.py | 31 +++++++++++++++++++ tagstudio/tests/test_library.py | 18 +++++++++++ 5 files changed, 101 insertions(+) diff --git a/tagstudio/src/core/library/alchemy/enums.py b/tagstudio/src/core/library/alchemy/enums.py index aaf9d32a..ce525019 100644 --- a/tagstudio/src/core/library/alchemy/enums.py +++ b/tagstudio/src/core/library/alchemy/enums.py @@ -77,6 +77,9 @@ class FilterState: path: Path | str | None = None # file name name: str | None = None + # file type + filetype: str | None = None + mediatype: str | None = None # a generic query to be parsed query: str | None = None @@ -87,6 +90,7 @@ class FilterState: # parse the value if ":" in query: kind, _, value = query.partition(":") + value = value.replace('"', "") else: # default to tag search kind, value = "tag", query @@ -101,6 +105,10 @@ class FilterState: self.name = value elif kind == "id": self.id = int(self.id) if str(self.id).isnumeric() else self.id + elif kind == "filetype": + self.filetype = value + elif kind == "mediatype": + self.mediatype = value else: self.tag = self.tag and self.tag.strip() diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index 34267eab..0aed7ce4 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -36,6 +36,7 @@ from ...constants import ( TS_FOLDER_NAME, ) from ...enums import LibraryPrefs +from ...media_types import MediaCategories from .db import make_tables from .enums import FieldTypeEnum, FilterState, TagColor from .fields import ( @@ -438,6 +439,18 @@ class Library: ) elif search.path: statement = statement.where(Entry.path.ilike(f"%{search.path}%")) + elif search.filetype: + statement = statement.where(Entry.suffix.ilike(f"{search.filetype}")) + elif search.mediatype: + extensions: set[str] = set[str]() + for media_cat in MediaCategories.ALL_CATEGORIES: + if search.mediatype == media_cat.name: + extensions = extensions | media_cat.extensions + break + # just need to map it to search db - suffixes do not have '.' + statement = statement.where( + Entry.suffix.in_(map(lambda x: x.replace(".", ""), extensions)) + ) extensions = self.prefs(LibraryPrefs.EXTENSION_LIST) is_exclude_list = self.prefs(LibraryPrefs.IS_EXCLUDE_LIST) diff --git a/tagstudio/src/core/media_types.py b/tagstudio/src/core/media_types.py index b88921f3..78755aac 100644 --- a/tagstudio/src/core/media_types.py +++ b/tagstudio/src/core/media_types.py @@ -62,6 +62,7 @@ class MediaCategory: media_type: MediaType extensions: set[str] + name: str is_iana: bool = False @@ -338,151 +339,181 @@ class MediaCategories: media_type=MediaType.ADOBE_PHOTOSHOP, extensions=_ADOBE_PHOTOSHOP_SET, is_iana=False, + name="photoshop", ) AFFINITY_PHOTO_TYPES: MediaCategory = MediaCategory( media_type=MediaType.AFFINITY_PHOTO, extensions=_AFFINITY_PHOTO_SET, is_iana=False, + name="affinity photo", ) ARCHIVE_TYPES: MediaCategory = MediaCategory( media_type=MediaType.ARCHIVE, extensions=_ARCHIVE_SET, is_iana=False, + name="archive", ) AUDIO_MIDI_TYPES: MediaCategory = MediaCategory( media_type=MediaType.AUDIO_MIDI, extensions=_AUDIO_MIDI_SET, is_iana=False, + name="audio midi", ) AUDIO_TYPES: MediaCategory = MediaCategory( media_type=MediaType.AUDIO, extensions=_AUDIO_SET | _AUDIO_MIDI_SET, is_iana=True, + name="audio", ) BLENDER_TYPES: MediaCategory = MediaCategory( media_type=MediaType.BLENDER, extensions=_BLENDER_SET, is_iana=False, + name="blender", ) DATABASE_TYPES: MediaCategory = MediaCategory( media_type=MediaType.DATABASE, extensions=_DATABASE_SET, is_iana=False, + name="database", ) DISK_IMAGE_TYPES: MediaCategory = MediaCategory( media_type=MediaType.DISK_IMAGE, extensions=_DISK_IMAGE_SET, is_iana=False, + name="disk image", ) DOCUMENT_TYPES: MediaCategory = MediaCategory( media_type=MediaType.DOCUMENT, extensions=_DOCUMENT_SET, is_iana=False, + name="document", ) EBOOK_TYPES: MediaCategory = MediaCategory( media_type=MediaType.EBOOK, extensions=_EBOOK_SET, is_iana=False, + name="ebook", ) FONT_TYPES: MediaCategory = MediaCategory( media_type=MediaType.FONT, extensions=_FONT_SET, is_iana=True, + name="font", ) IMAGE_ANIMATED_TYPES: MediaCategory = MediaCategory( media_type=MediaType.IMAGE_ANIMATED, extensions=_IMAGE_ANIMATED_SET, is_iana=False, + name="animated image", ) IMAGE_RAW_TYPES: MediaCategory = MediaCategory( media_type=MediaType.IMAGE_RAW, extensions=_IMAGE_RAW_SET, is_iana=False, + name="raw image", ) IMAGE_VECTOR_TYPES: MediaCategory = MediaCategory( media_type=MediaType.IMAGE_VECTOR, extensions=_IMAGE_VECTOR_SET, is_iana=False, + name="vector image", ) IMAGE_RASTER_TYPES: MediaCategory = MediaCategory( media_type=MediaType.IMAGE, extensions=_IMAGE_RASTER_SET, is_iana=False, + name="raster image", ) IMAGE_TYPES: MediaCategory = MediaCategory( media_type=MediaType.IMAGE, extensions=_IMAGE_RASTER_SET | _IMAGE_RAW_SET | _IMAGE_VECTOR_SET, is_iana=True, + name="image", ) INSTALLER_TYPES: MediaCategory = MediaCategory( media_type=MediaType.INSTALLER, extensions=_INSTALLER_SET, is_iana=False, + name="installer", ) MATERIAL_TYPES: MediaCategory = MediaCategory( media_type=MediaType.MATERIAL, extensions=_MATERIAL_SET, is_iana=False, + name="material", ) MODEL_TYPES: MediaCategory = MediaCategory( media_type=MediaType.MODEL, extensions=_MODEL_SET, is_iana=True, + name="model", ) OPEN_DOCUMENT_TYPES: MediaCategory = MediaCategory( media_type=MediaType.OPEN_DOCUMENT, extensions=_OPEN_DOCUMENT_SET, is_iana=False, + name="open document", ) PACKAGE_TYPES: MediaCategory = MediaCategory( media_type=MediaType.PACKAGE, extensions=_PACKAGE_SET, is_iana=False, + name="package", ) PDF_TYPES: MediaCategory = MediaCategory( media_type=MediaType.PDF, extensions=_PDF_SET, is_iana=False, + name="pdf", ) PLAINTEXT_TYPES: MediaCategory = MediaCategory( media_type=MediaType.PLAINTEXT, extensions=_PLAINTEXT_SET, is_iana=False, + name="plaintext", ) PRESENTATION_TYPES: MediaCategory = MediaCategory( media_type=MediaType.PRESENTATION, extensions=_PRESENTATION_SET, is_iana=False, + name="presentation", ) PROGRAM_TYPES: MediaCategory = MediaCategory( media_type=MediaType.PROGRAM, extensions=_PROGRAM_SET, is_iana=False, + name="program", ) SHORTCUT_TYPES: MediaCategory = MediaCategory( media_type=MediaType.SHORTCUT, extensions=_SHORTCUT_SET, is_iana=False, + name="shortcut", ) SOURCE_ENGINE_TYPES: MediaCategory = MediaCategory( media_type=MediaType.SOURCE_ENGINE, extensions=_SOURCE_ENGINE_SET, is_iana=False, + name="source engine", ) SPREADSHEET_TYPES: MediaCategory = MediaCategory( media_type=MediaType.SPREADSHEET, extensions=_SPREADSHEET_SET, is_iana=False, + name="spreadsheet", ) TEXT_TYPES: MediaCategory = MediaCategory( media_type=MediaType.TEXT, extensions=_DOCUMENT_SET | _PLAINTEXT_SET, is_iana=True, + name="text", ) VIDEO_TYPES: MediaCategory = MediaCategory( media_type=MediaType.VIDEO, extensions=_VIDEO_SET, is_iana=True, + name="video", ) ALL_CATEGORIES: list[MediaCategory] = [ diff --git a/tagstudio/tests/conftest.py b/tagstudio/tests/conftest.py index efcc2c66..39c3f753 100644 --- a/tagstudio/tests/conftest.py +++ b/tagstudio/tests/conftest.py @@ -21,6 +21,37 @@ def cwd(): return CWD +@pytest.fixture +def file_mediatypes_library(): + lib = Library() + + status = lib.open_library(pathlib.Path(""), ":memory:") + assert status.success + + entry1 = Entry( + folder=lib.folder, + path=pathlib.Path("foo.png"), + fields=lib.default_fields, + ) + + entry2 = Entry( + folder=lib.folder, + path=pathlib.Path("bar.png"), + fields=lib.default_fields, + ) + + entry3 = Entry( + folder=lib.folder, + path=pathlib.Path("baz.apng"), + fields=lib.default_fields, + ) + + assert lib.add_entries([entry1, entry2, entry3]) + assert len(lib.tags) == 2 + + return lib + + @pytest.fixture def library(request): # when no param is passed, use the default diff --git a/tagstudio/tests/test_library.py b/tagstudio/tests/test_library.py index 81f26690..db28a72a 100644 --- a/tagstudio/tests/test_library.py +++ b/tagstudio/tests/test_library.py @@ -401,3 +401,21 @@ def test_library_prefs_multiple_identical_vals(): # accessing .value should raise exception with pytest.raises(AttributeError): assert TestPrefs.BAR.value + + +@pytest.mark.parametrize(["filetype", "num_of_filetype"], [("md", 1), ("txt", 1), ("png", 0)]) +def test_filetype_search(library, filetype, num_of_filetype): + results = library.search_library(FilterState(filetype=filetype)) + assert len(results.items) == num_of_filetype + + +@pytest.mark.parametrize(["filetype", "num_of_filetype"], [("png", 2), ("apng", 1), ("ng", 0)]) +def test_filetype_return_one_filetype(file_mediatypes_library, filetype, num_of_filetype): + results = file_mediatypes_library.search_library(FilterState(filetype=filetype)) + assert len(results.items) == num_of_filetype + + +@pytest.mark.parametrize(["mediatype", "num_of_mediatype"], [("plaintext", 2), ("image", 0)]) +def test_mediatype_search(library, mediatype, num_of_mediatype): + results = library.search_library(FilterState(mediatype=mediatype)) + assert len(results.items) == num_of_mediatype