Skip to content

feat: add filename sorting #842

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

Merged
merged 12 commits into from
Mar 13, 2025
2 changes: 1 addition & 1 deletion docs/updates/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ These version milestones are rough estimations for when the previous core featur
- [ ] Field content search [HIGH]
- [ ] Sort by date created [HIGH]
- [ ] Sort by date modified [HIGH]
- [ ] Sort by filename [HIGH]
- [x] Sort by filename [HIGH]
- [ ] HAS operator for composition tags [HIGH]
- [ ] Search bar rework
- [ ] Improved tag autocomplete [HIGH]
Expand Down
2 changes: 1 addition & 1 deletion src/tagstudio/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ class LibraryPrefs(DefaultEnum):
IS_EXCLUDE_LIST = True
EXTENSION_LIST = [".json", ".xmp", ".aae"]
PAGE_SIZE = 500
DB_VERSION = 8
DB_VERSION = 9
2 changes: 2 additions & 0 deletions src/tagstudio/core/library/alchemy/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class ItemType(enum.Enum):

class SortingModeEnum(enum.Enum):
DATE_ADDED = "file.date_added"
FILE_NAME = "generic.filename"
PATH = "file.path"


@dataclass
Expand Down
44 changes: 43 additions & 1 deletion src/tagstudio/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,12 +472,25 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus:

# Apply any post-SQL migration patches.
if not is_new:
# save backup if patches will be applied
if LibraryPrefs.DB_VERSION.default != db_version:
self.library_dir = library_dir
self.save_library_backup_to_disk()
self.library_dir = None

# schema changes first
if db_version < 8:
self.apply_db8_schema_changes(session)
if db_version < 9:
self.apply_db9_schema_changes(session)

# now the data changes
if db_version == 6:
self.apply_repairs_for_db6(session)
if db_version >= 6 and db_version < 8:
self.apply_db8_default_data(session)
if db_version < 9:
self.apply_db9_filename_population(session)

# Update DB_VERSION
if LibraryPrefs.DB_VERSION.default > db_version:
Expand Down Expand Up @@ -580,6 +593,29 @@ def apply_db8_default_data(self, session: Session):
)
session.rollback()

def apply_db9_schema_changes(self, session: Session):
"""Apply database schema changes introduced in DB_VERSION 9."""
add_filename_column = text(
"ALTER TABLE entries ADD COLUMN filename TEXT NOT NULL DEFAULT ''"
)
try:
session.execute(add_filename_column)
session.commit()
logger.info("[Library][Migration] Added filename column to entries table")
except Exception as e:
logger.error(
"[Library][Migration] Could not create filename column in entries table!",
error=e,
)
session.rollback()

def apply_db9_filename_population(self, session: Session):
"""Populate the filename column introduced in DB_VERSION 9."""
for entry in self.get_entries():
session.merge(entry).filename = entry.path.name
session.commit()
logger.info("[Library][Migration] Populated filename column in entries table")

@property
def default_fields(self) -> list[BaseField]:
with Session(self.engine) as session:
Expand Down Expand Up @@ -852,14 +888,18 @@ def search_library(
statement = statement.distinct(Entry.id)
start_time = time.time()
query_count = select(func.count()).select_from(statement.alias("entries"))
count_all: int = session.execute(query_count).scalar()
count_all: int = session.execute(query_count).scalar() or 0
end_time = time.time()
logger.info(f"finished counting ({format_timespan(end_time - start_time)})")

sort_on: ColumnExpressionArgument = Entry.id
match search.sorting_mode:
case SortingModeEnum.DATE_ADDED:
sort_on = Entry.id
case SortingModeEnum.FILE_NAME:
sort_on = func.lower(Entry.filename)
case SortingModeEnum.PATH:
sort_on = func.lower(Entry.path)

statement = statement.order_by(asc(sort_on) if search.ascending else desc(sort_on))
statement = statement.limit(search.limit).offset(search.offset)
Expand Down Expand Up @@ -1371,6 +1411,8 @@ def save_library_backup_to_disk(self) -> Path:
target_path,
)

logger.info("Library backup saved to disk.", path=target_path)

return target_path

def get_tag(self, tag_id: int) -> Tag | None:
Expand Down
2 changes: 2 additions & 0 deletions src/tagstudio/core/library/alchemy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class Entry(Base):
folder: Mapped[Folder] = relationship("Folder")

path: Mapped[Path] = mapped_column(PathType, unique=True)
filename: Mapped[str] = mapped_column()
suffix: Mapped[str] = mapped_column()
date_created: Mapped[dt | None]
date_modified: Mapped[dt | None]
Expand Down Expand Up @@ -232,6 +233,7 @@ def __init__(
self.path = path
self.folder = folder
self.id = id
self.filename = path.name
self.suffix = path.suffix.lstrip(".").lower()

# The date the file associated with this entry was created.
Expand Down
1 change: 1 addition & 0 deletions src/tagstudio/resources/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"file.date_added": "Hinzufügungsdatum",
"file.date_created": "Erstellungsdatum",
"file.date_modified": "Datum geändert",
"file.path": "Dateipfad",
"file.dimensions": "Abmessungen",
"file.duplicates.description": "TagStudio unterstützt das Importieren von DupeGuru-Ergebnissen um Dateiduplikate zu verwalten.",
"file.duplicates.dupeguru.advice": "Nach dem Kopiervorgang kann DupeGuru benutzt werden und ungewollte Dateien zu löschen. Anschließend kann TagStudios \"Unverknüpfte Einträge reparieren\" Funktion im \"Werkzeuge\" Menü benutzt werden um die nicht verknüpften Einträge zu löschen.",
Expand Down
1 change: 1 addition & 0 deletions src/tagstudio/resources/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"file.date_added": "Date Added",
"file.date_created": "Date Created",
"file.date_modified": "Date Modified",
"file.path": "File Path",
"file.dimensions": "Dimensions",
"file.duplicates.description": "TagStudio supports importing DupeGuru results to manage duplicate files.",
"file.duplicates.dupeguru.advice": "After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's \"Fix Unlinked Entries\" feature in the Tools menu in order to delete the unlinked Entries.",
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ def library(request):
@pytest.fixture
def search_library() -> Library:
lib = Library()
lib.open_library(Path(CWD / "fixtures" / "search_library"))
status = lib.open_library(Path(CWD / "fixtures" / "search_library"))
assert status.success
return lib


Expand Down
Binary file not shown.
Binary file modified tests/fixtures/search_library/.TagStudio/ts_library.sqlite
Binary file not shown.
1 change: 1 addition & 0 deletions tests/test_db_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_6")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_7")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_8")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_9")),
],
)
def test_library_migrations(path: str):
Expand Down