Compare commits

..

1 Commits

Author SHA1 Message Date
Travis Abendshien ed9850e60f feat!: add file date metadata to db 2026-06-30 16:40:24 -07:00
23 changed files with 963 additions and 812 deletions
+43 -61
View File
@@ -18,9 +18,9 @@ Legacy (JSON) library save format versions were tied to the release version of t
### Versions 1.0.0 - 9.4.2
| Used in Releases | Format | Location |
| ---------------- | ------ | --------------------------------------------- |
| v1.0.0 - v9.4.2 | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
| Used From | Format | Location |
| --------- | ------ | --------------------------------------------- |
| v1.0.0 | JSON | `<Library Folder>`/.TagStudio/ts_library.json |
The legacy database format for public TagStudio releases [v9.1](https://github.com/TagStudioDev/TagStudio/tree/Alpha-v9.1) through [v9.4.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.4.2). Variations of this format had been used privately since v1.0.0.
@@ -30,11 +30,7 @@ Replaced by the new SQLite format introduced in TagStudio [v9.5.0 Pre-Release 1]
## SQLite <small>v9.5.0+</small>
Starting with TagStudio [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1), the library save format has been moved to the [SQLite](https://sqlite.org) format. Legacy JSON libraries are migrated (with the user's consent) to the new format when opening in current versions of the program. The save format versioning is now separate from the program's versioning number.
The database storage location of all SQLite versions to date is at:
`<Library Folder>`/.TagStudio/ts_library.sqlite
Starting with TagStudio [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1), the library save format has been moved to a [SQLite](https://sqlite.org) format. Legacy JSON libraries are migrated (with the user's consent) to the new format when opening in current versions of the program. The save format versioning is now separate from the program's versioning number.
### Versioning
@@ -42,12 +38,12 @@ Versions **1-100** stored the database version in a table called `preferences` i
Versions **>101** store the database version in a table called `versions` in a row with the `key` column of `'CURRENT'` inside the corresponding `value` column. The `versions` table also stores the initial database version in which the file was created with under the `'INITIAL'` key. Databases created before this key was introduced will always have `'INITIAL'` value of `100`.
#### `versions` Table
#### "versions" Table
| key (`VARCHAR`) | value (`INTEGER`) |
| --------------- | ------------------------------------------ |
| `'INITIAL'` | Version DB was created with, minimum `100` |
| `'CURRENT'` | Current version of DB |
| key (`VARCHAR`) | value (`INTEGER`) |
| --------------- | --------------------------------------------- |
| `'INITIAL'` | <Version DB was created with, minimum `100`\> |
| `'CURRENT'` | <Current version of DB\> |
#### Major and Minor Versioning
@@ -55,23 +51,19 @@ Version **100** came along with a major/minor versioning system built into to th
For example, a database with version 204 would still be readable in an older version of TagStudio that understands version 200. A database with version 300, on the other hand, would no longer be readable in that same older version and an error message would display.
<!-- prettier-ignore -->
!!! note ""Version 0" Message"
If you see an message when opening a library along the lines of **"Found Version 0"**, this means that you're opening a library created with a newer version of TagStudio in an older version of TagStudio that does not recognize the future versioning system. To open your library, you should use a TagStudio version greater than or equal to the one the library was created or last used with.
---
### Versions 1 - 5
These versions were used while developing the new SQLite file format, outside of any official or recommended release. These versions **were never supported** in any official capacity and were actively warned against using for real libraries.
These versions were used while developing the new SQLite file format, outside any official or recommended release. These versions **were never supported** in any official capacity and were actively warned against using for real libraries.
---
### Version 6
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ------------------------------------------------------------------------------- | ------ |
| d1b006a8978a7fa1b7f1a82243e490aca8a8355e | [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite |
| Used From | Format | Location |
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.0-pr1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr1) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
The first public version of the SQLite save file format.
@@ -81,9 +73,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
### Version 7
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ------------------------------------------------------------------------------- | ------ |
| 480328b83bc1c69ab52a3eb11e2935337d3460ab | [v9.5.0-pr2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | SQLite |
| Used From | Format | Location |
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.0-pr2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr2) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- ~~Repairs "Description" fields to use a TEXT_LINE key instead of a TEXT_BOX key.~~ _See [Version 200](#version-200)_
- Repairs tags that may have a disambiguation_id pointing towards a deleted tag.
@@ -92,9 +84,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
### Version 8
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ------------------------------------------------------------------------------- | ------ |
| 28de21ade757aa5b80e87f77a459f4a3af21ffe0 | [v9.5.0-pr4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | SQLite |
| Used From | Format | Location |
| ------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.0-pr4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.0-pr4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Adds the `color_border` column to the `tag_colors` table. Used for instructing the [secondary color](colors.md#secondary-color) to apply to a tag's border as a new optional behavior.
- Adds three new default colors: "Burgundy (TagStudio Shades)", "Dark Teal (TagStudio Shades)", and "Dark Lavender (TagStudio Shades)".
@@ -104,9 +96,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
### Version 9
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 31833245a4d7a655dc4e66e6005a9dd78c36c04d | [v9.5.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.2) | SQLite |
| Used From | Format | Location |
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| [v9.5.2](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.2) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Adds the `filename` column to the `entries` table. Used for sorting entries by filename in search results.
@@ -116,9 +108,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 100
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | --------------------- | ------ |
| 74383e3c3c12f72be1481ab0b86c7360b95c2d85 | _None_ | SQLite |
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| 74383e3c3c12f72be1481ab0b86c7360b95c2d85 | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Introduces built-in minor versioning
- The version number divided by 100 (and floored) constitutes the **major** version. Major version indicate breaking changes that prevent libraries from being opened in TagStudio versions older than the ones they were created in.
@@ -127,9 +119,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 101
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 12e074b71d8860282b44e49e0e1a41b7a2e4bae8 | [v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite |
| Used From | Format | Location |
| ---------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| 12e074b71d8860282b44e49e0e1a41b7a2e4bae8/[v9.5.4](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.4) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Deprecates the `preferences` table, set to be removed in a [future](#version-104) TagStudio version.
- Introduces the `versions` table
@@ -141,26 +133,24 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 102
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 71d04254cf87f4200bb7ffc81656e50dfb122e4d | [v9.5.5](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.5) | SQLite |
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| 71d04254cf87f4200bb7ffc81656e50dfb122e4d | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Applies repairs to the `tag_parents` table created in [version 100](#version-100), removing rows that reference tags that have been deleted.
#### Version 103
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 88d0b47a86821ccfadba653f30a515abce5b24b0 | [v9.5.7](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.7) | SQLite |
| Used From | Format | Location |
| ---------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------- |
| 88d0b47a86821ccfadba653f30a515abce5b24b0/[v9.5.7](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.5.7) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Adds the `is_hidden` column to the `tags` table (default `0`). Used for excluding entries tagged with hidden tags from library searches.
- Sets the `is_hidden` field on the built-in Archived tag to `1`, to match the Archived tag now being hidden by default.
#### Version 104
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | --------------------- | ------ |
| ad2cbbca483018d245b44348e2c4f5a0e0bb28f1 | _None_ | SQLite |
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| ad2cbbca483018d245b44348e2c4f5a0e0bb28f1 | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Removes the `preferences` table, after migrating the contained extension list to the .ts_ignore file, if necessary.
@@ -170,9 +160,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 200
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | --------------------- | ------ |
| c15e2b56eedd0a3c13391fa43571b8f8f7c7a91f | _None_ | SQLite |
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| c15e2b56eedd0a3c13391fa43571b8f8f7c7a91f | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Adds `text_field_templates` and `date_field_templates` tables.
- Drops `boolean_fields` and `value_type` tables.
@@ -187,17 +177,9 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
#### Version 201
| Added in Commit | Introduced in Release | Format |
| ---------------------------------------- | ----------------------------------------------------------------------- | ------ |
| 38da7bb3a920a01d4d70fa065fd19c83ff6eecb1 | [v9.6.0](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.6.0) | SQLite |
| Used From | Format | Location |
| ---------------------------------------- | ------ | ----------------------------------------------- |
| 38da7bb3a920a01d4d70fa065fd19c83ff6eecb1 | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
- Drops `type_key` columns from `text_fields` and `datetime_fields` tables.
- Enforces column positions for `text_fields` and `datetime_fields` tables.
#### Version 202
| Added in Commit | Introduced in Release | Format |
| --------------- | ----------------------------------------------------------------------- | ------ |
| | [v9.6.1](https://github.com/TagStudioDev/TagStudio/releases/tag/v9.6.1) | SQLite |
- Applies repairs to the `tag_parents` table, removing rows that reference child tags that have been deleted.
+4 -3
View File
@@ -122,6 +122,7 @@ A detailed specification written for the TagStudio tag and/or library format. In
- [ ] Tag Search
- [ ] Pinned Tags
- [ ] New Tabbed Tag Building UI to Support New Tag Features :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.8.x]**
- [ ] Custom Thumbnail Overrides :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.7.x]**
- [ ] Media Duration Labels :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.6.x]**
- [ ] Word/Line Count Labels :material-chevron-up:{ .priority-low title="Low Priority" }
- [ ] Custom Tag Badges :material-chevron-up:{ .priority-low title="Low Priority" }
@@ -175,12 +176,12 @@ File or file-like [entries](entries.md) stored in the library.
- [ ] Optional Units (e.g. inches, cm, height notation, degrees, bytes, etc.) :material-chevron-double-up:{ .priority-med title="Medium Priority" }
- [x] Custom Field Names **[[v9.6.0](changelog.md#960-june-29th-2026)]**
- [x] Removal of Deprecated Fields **[[v9.6.0](changelog.md#960-june-29th-2026)]**
- [ ] Custom Thumbnail Overrides :material-chevron-double-up:{ .priority-med title="Medium Priority" } **[v9.7.x]**
- [ ] Entry Groups :material-chevron-triple-up:{ .priority-high title="High Priority" } **[v9.7.x]**
- [ ] Non-exclusive; Entries can be in multiple groups :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Ability to number entries within group (i.e. page numbers) :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Ability to number entries within group :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Ability to set sorting method for group :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Ability to set custom thumbnail for group :material-chevron-double-up:{ .priority-med title="Medium Priority" }
- [ ] Ability to set custom thumbnail for group :material-chevron-triple-up:{ .priority-high title="High Priority" }
- [ ] Group is treated as entry with tags and metadata :material-chevron-double-up:{ .priority-med title="Medium Priority" }
- [ ] Nested groups :material-chevron-double-up:{ .priority-med title="Medium Priority" }
### :material-tag-text: Tags
+1 -1
View File
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
{
lib,
pkgs,
@@ -77,7 +78,6 @@ pkgs.mkShellNoCC {
coreutils
uv
pyright
ruff
];
buildInputs = [
+1 -2
View File
@@ -30,7 +30,7 @@ dependencies = [
"PySide6==6.8.0.*",
"rarfile==4.2",
"rawpy~=0.27",
"Send2Trash>=1.8,<3",
"Send2Trash~=1.8",
"SQLAlchemy~=2.0",
"srctools~=2.6",
"structlog~=25.3",
@@ -69,7 +69,6 @@ qt_api = "pyside6"
pythonpath = ["src"]
filterwarnings = [
"ignore:DISTINCT ON is currently supported only by the PostgreSQL dialect:sqlalchemy.exc.SADeprecationWarning",
'ignore:Invoking or_\(\) without arguments is deprecated:sqlalchemy.exc.SADeprecationWarning',
]
[tool.pyright]
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from tagstudio.main import main
if __name__ == "__main__":
main()
@@ -9,7 +9,7 @@ JSON_FILENAME: str = "ts_library.json"
DB_VERSION_CURRENT_KEY: str = "CURRENT"
DB_VERSION_INITIAL_KEY: str = "INITIAL"
DB_VERSION: int = 202
DB_VERSION: int = 201
TAG_CHILDREN_QUERY = text("""
WITH RECURSIVE ChildTags AS (
+6 -10
View File
@@ -42,17 +42,13 @@ def make_engine(connection_string: str) -> Engine:
def make_tables(engine: Engine) -> None:
logger.info("[Library] Creating DB tables...")
with engine.connect() as conn:
# TODO: this should instead be migrations that create the exact tables that were added in
# the respective DB versions
Base.metadata.create_all(conn)
conn.commit()
Base.metadata.create_all(engine)
# TODO: this needs to be a migration
# tag IDs < 1000 are reserved
# create tag and delete it to bump the autoincrement sequence
# TODO - find a better way
# is this the better way?
# tag IDs < 1000 are reserved
# create tag and delete it to bump the autoincrement sequence
# TODO - find a better way
# is this the better way?
with engine.connect() as conn:
result = conn.execute(text("SELECT SEQ FROM sqlite_sequence WHERE name='tags'"))
autoincrement_val = result.scalar()
if not autoincrement_val or autoincrement_val <= RESERVED_TAG_END:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
from __future__ import annotations
from datetime import datetime as dt
from pathlib import Path
from typing import TYPE_CHECKING, Any, override
from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, null
from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
from tagstudio.core.library.alchemy.db import Base, PathType
from tagstudio.core.library.alchemy.joins import TagParent
if TYPE_CHECKING:
from tagstudio.core.library.alchemy.models import Entry
class FileMetadata(Base):
"""Table that includes file data and metadata obtained from os.stat() for entries."""
__tablename__ = "file_metadata"
entry_id: Mapped[int] = mapped_column(
ForeignKey("entries.id"), primary_key=True, nullable=False
)
# NOTE: These dates are stored as floats because that's their natural form from os.stat()
# and comparisons are quicker without having to convert to/from datetime objects.
date_created: Mapped[float | None]
date_modified: Mapped[float | None]
def __init__(
self,
entry_id: int,
date_created: float | None = None,
date_modified: float | None = None,
) -> None:
super().__init__()
self.entry_id = entry_id
# # Path data
# self.path = path
# self.filename = path.name
# self.suffix = path.suffix.lstrip(".").lower()
# File metadata
self.date_created = date_created # st_birthtime on Windows and Mac, st_ctime on Linux
self.date_modified = date_modified # st_mtime
class ExifMetadata(Base):
"""Contains Exif metadata for a entries."""
__tablename__ = "exif_metadata"
entry_id: Mapped[int] = mapped_column(
ForeignKey("entries.id"), primary_key=True, nullable=False
)
date_taken: Mapped[dt | None]
def __init__(
self,
entry_id: int,
date_taken: dt | None = None,
) -> None:
super().__init__()
self.entry_id = entry_id
self.date_taken = date_taken # Exif.Image.DateTime
class DimensionMetadata(Base):
"""Contains dimension metadata for entries (e.g. image and video files)."""
__tablename__ = "dimension_metadata"
entry_id: Mapped[int] = mapped_column(
ForeignKey("entries.id"), primary_key=True, nullable=False
)
width: Mapped[int] = mapped_column(nullable=False)
height: Mapped[int] = mapped_column(nullable=False)
def __init__(
self,
entry_id: int,
width: int,
height: int,
) -> None:
super().__init__()
self.entry_id = entry_id
self.width = width
self.height = height
class DurationMetadata(Base):
"""Contains duration metadata for entries (e.g. audio and video files)."""
__tablename__ = "duration_metadata"
entry_id: Mapped[int] = mapped_column(
ForeignKey("entries.id"), primary_key=True, nullable=False
)
duration: Mapped[float] = mapped_column(nullable=False)
def __init__(
self,
entry_id: int,
duration: float,
) -> None:
super().__init__()
self.entry_id = entry_id
self.duration = duration
+35 -14
View File
@@ -17,6 +17,8 @@ from tagstudio.core.library.alchemy.fields import (
TextField,
)
from tagstudio.core.library.alchemy.joins import TagParent
from tagstudio.core.library.alchemy.metadata import FileMetadata
from tagstudio.core.utils.stat import get_date_created, get_date_modified
class Namespace(Base):
@@ -182,6 +184,7 @@ class Tag(Base):
return self.name >= other.name
# TODO: Use or replace these with an actual multi-root implementation
class Folder(Base):
__tablename__ = "folders"
@@ -196,15 +199,16 @@ class Entry(Base):
id: Mapped[int] = mapped_column(primary_key=True)
# TODO: Use or replace these with an actual multi-root implementation
folder_id: Mapped[int] = mapped_column(ForeignKey("folders.id"))
folder: Mapped[Folder] = relationship("Folder")
# TODO: Possibly move to FileMetadata table if Entry is split into Entry/FileEntry (see #588)
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]
date_added: Mapped[dt | None]
date_added: Mapped[dt | None] # The date this entry was added to the library
tags: Mapped[set[Tag]] = relationship(secondary="tag_entries")
@@ -217,6 +221,11 @@ class Entry(Base):
cascade="all, delete",
)
file_metadata: Mapped["FileMetadata"] = relationship(
uselist=False,
cascade="all, delete-orphan",
)
@property
def fields(self) -> list[BaseField]:
fields: list[BaseField] = []
@@ -232,30 +241,35 @@ class Entry(Base):
def is_archived(self) -> bool:
return any(tag.id == TAG_ARCHIVED for tag in self.tags)
@property
def date_created(self) -> float | None:
return self.file_metadata.date_created if self.file_metadata else None
@property
def date_modified(self) -> float | None:
return self.file_metadata.date_modified if self.file_metadata else None
def __init__(
self,
path: Path,
folder: Folder,
fields: list[BaseField],
id: int | None = None,
date_created: dt | None = None,
date_modified: dt | None = None,
date_added: dt | None = None,
# date_created: float | None = None,
# date_modified: float | None = None,
path_for_file_metadata: Path | None = None,
) -> None:
super().__init__()
self.path = path
self.folder = folder
self.id = id # pyright: ignore[reportAttributeAccessIssue]
self.folder = folder # NOTE: Currently unused
self.path = path
self.filename = path.name
self.suffix = path.suffix.lstrip(".").lower()
# The date the file associated with this entry was created.
# st_birthtime on Windows and Mac, st_ctime on Linux.
self.date_created = date_created
# The date the file associated with this entry was last modified: st_mtime.
self.date_modified = date_modified
# The date this entry was added to the library.
self.date_added = date_added
self.date_added = date_added # The date this entry was added to the library
for field in fields:
if isinstance(field, TextField):
@@ -265,6 +279,13 @@ class Entry(Base):
else:
raise ValueError(f"Invalid field type: {field}")
if path_for_file_metadata:
self.file_metadata = FileMetadata(
entry_id=self.id,
date_created=get_date_created(path_for_file_metadata),
date_modified=get_date_modified(path_for_file_metadata),
)
def has_tag(self, tag: Tag) -> bool:
return tag in self.tags
+12 -2
View File
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
from datetime import datetime as dt
from pathlib import Path
from time import time
import platform
import structlog
from wcmatch import pathlib
@@ -38,12 +39,14 @@ class RefreshTracker:
while index < len(self.files_not_in_library):
yield index
end = min(len(self.files_not_in_library), index + batch_size)
lib_dir = unwrap(self.library.library_dir)
entries = [
Entry(
path=entry_path,
folder=unwrap(self.library.folder),
fields=[],
date_added=dt.now(),
path_for_file_metadata=(lib_dir / entry_path),
)
for entry_path in self.files_not_in_library[index:end]
]
@@ -144,8 +147,11 @@ class RefreshTracker:
dir_file_count += 1
self.library.included_files.add(f)
if not self.library.has_entry_with_path(f):
entry_id = self.library.get_entry_id_from_path(f)
if entry_id < 0:
self.files_not_in_library.append(f)
else:
self.library.refresh_file_entry_stats(entry_id, path=f)
end_time_total = time()
yield dir_file_count
@@ -189,8 +195,12 @@ class RefreshTracker:
relative_path = f.relative_to(library_dir)
if not self.library.has_entry_with_path(relative_path):
entry_id = self.library.get_entry_id_from_path(relative_path)
if entry_id < 0:
self.files_not_in_library.append(relative_path)
else:
self.library.refresh_file_entry_stats(entry_id, path=relative_path)
except ValueError:
logger.info("[Refresh]: ValueError when refreshing directory with wcmatch!")
+16
View File
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
import platform
from pathlib import Path
def get_date_modified(path: Path) -> float:
return path.stat().st_mtime
def get_date_created(path: Path) -> float:
if platform.system() in {"Windows", "Darwin"}:
return path.stat().st_birthtime
else:
return path.stat().st_ctime
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env python
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
@@ -11,7 +11,6 @@ from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchModal
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
from tagstudio.qt.previews.vendored.ffmpeg import FFMPEG_CMD, FFPROBE_CMD
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
if typing.TYPE_CHECKING:
@@ -23,10 +22,7 @@ class PreviewPanel(PreviewPanelView):
super().__init__(library, driver)
self.__add_field_modal = FieldTemplateSearchModal(self.lib, is_field_template_chooser=True)
self.__add_tag_modal = TagSearchModal(
self.lib, title=Translations["tag.add.plural"], is_tag_chooser=True
)
self.__add_tag_modal.tsp.set_driver(driver)
self.__add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
self._thumb.check_ffmpeg.connect(self._toggle_ffmpeg_warning)
@typing.override
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-only
from typing import override
from typing import TYPE_CHECKING, override
from warnings import catch_warnings
import structlog
@@ -20,6 +20,10 @@ from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
logger = structlog.get_logger(__name__)
# Only import for type checking/autocompletion, will not be imported at runtime.
if TYPE_CHECKING:
pass
class TagSearchModal(PanelModal):
tsp: "TagSearchPanel"
@@ -27,7 +31,6 @@ class TagSearchModal(PanelModal):
def __init__(
self,
library: Library,
title: str,
exclude: list[int] | None = None,
is_tag_chooser: bool = True,
has_save: bool = False,
@@ -39,8 +42,8 @@ class TagSearchModal(PanelModal):
view=TagSearchPanelView(is_tag_chooser),
)
super().__init__(
widget=self.tsp,
title=title,
self.tsp,
Translations["tag.add.plural"],
is_savable=has_save,
)
@@ -166,25 +169,17 @@ class TagSearchPanel(SearchPanel[Tag]):
# Connect callbacks
tag_widget.on_edit.connect(lambda edit_tag=item: self.on_item_edit(edit_tag))
tag_widget.on_remove.connect(lambda remove_tag=item: self._on_item_remove(remove_tag))
if self.is_chooser:
tag_widget.bg_button.clicked.connect(
lambda checked=False, tag=item: self._on_item_chosen(tag)
)
else:
tag_widget.bg_button.clicked.connect(
lambda checked=False, edit_tag=item: self.on_item_edit(edit_tag)
)
tag_widget.bg_button.clicked.connect(
lambda checked=False, tag=item: self._on_item_chosen(tag)
)
# Connect search action
if self._driver is not None:
tag_widget.search_for_tag_action.triggered.connect(
lambda checked=False, tag_id=item.id: self.search_for_tag(tag_id)
lambda tag_id=item.id: self.search_for_tag(tag_id)
)
tag_widget.search_for_tag_action.setEnabled(True)
else:
logger.warning(
"[TagSearchPanel] No driver was set for this TagSearchPanel. Was this on purpose?"
)
tag_widget.search_for_tag_action.setEnabled(False)
@override
+1 -3
View File
@@ -176,9 +176,7 @@ class BuildTagPanel(PanelWidget):
if tag is not None:
exclude_ids.append(tag.id)
self.add_tag_modal = TagSearchModal(
self.lib, title=Translations["tag.add.plural"], exclude=exclude_ids
)
self.add_tag_modal = TagSearchModal(self.lib, exclude_ids)
self.add_tag_modal.tsp.item_chosen.connect(lambda x: self._add_parent_tag_callback(x))
self.parent_tags_add_button.clicked.connect(self.add_tag_modal.show)
+5 -6
View File
@@ -395,15 +395,14 @@ class JsonMigrationModal(QObject):
# Convert JSON Library to SQLite
yield Translations["json_migration.creating_database_tables"]
self.sql_lib = SqliteLibrary()
temp_filename = "migration_ts_library.sqlite"
self.temp_path: Path = self.json_lib.library_dir / TS_FOLDER_NAME / temp_filename
self.temp_path: Path = (
self.json_lib.library_dir / TS_FOLDER_NAME / "migration_ts_library.sqlite"
)
if self.temp_path.exists():
logger.info('Temporary migration file "temp_path" already exists. Removing...')
self.temp_path.unlink()
self.sql_lib.create_sqlite_library(
self.json_lib.library_dir,
in_memory=False,
sql_filename=temp_filename,
self.sql_lib.open_sqlite_library(
self.json_lib.library_dir, is_new=True, storage_path=str(self.temp_path)
)
yield Translations.format(
"json_migration.migrating_files_entries", entries=len(self.json_lib.entries)
+86 -114
View File
@@ -98,9 +98,6 @@ class SettingsPanel(PanelWidget):
self.root_layout.setContentsMargins(0, 6, 0, 0)
self.library_settings_container = QWidget()
self.appearance_settings_container = QWidget()
self.localization_settings_container = QWidget()
self.media_settings_container = QWidget()
# Tabs
self.tab_widget = QTabWidget()
@@ -108,19 +105,6 @@ class SettingsPanel(PanelWidget):
self.__build_global_settings()
self.tab_widget.addTab(self.global_settings_container, Translations["settings.global"])
self.__build_appearance_settings()
self.tab_widget.addTab(
self.appearance_settings_container, Translations["settings.appearance"]
)
self.__build_localization_settings()
self.tab_widget.addTab(
self.localization_settings_container, Translations["settings.localization"]
)
self.__build_media_settings()
self.tab_widget.addTab(self.media_settings_container, Translations["settings.media"])
# self.__build_library_settings()
# self.tab_widget.addTab(self.library_settings_container, Translations["settings.library"])
@@ -148,6 +132,17 @@ class SettingsPanel(PanelWidget):
form_layout = QFormLayout(self.global_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
# Language
self.language_combobox = QComboBox()
for k in LANGUAGES:
self.language_combobox.addItem(k, LANGUAGES[k])
current_lang: str = self.driver.settings.language
if current_lang not in LANGUAGES.values():
current_lang = DEFAULT_TRANSLATION
self.language_combobox.setCurrentIndex(list(LANGUAGES.values()).index(current_lang))
self.language_combobox.currentIndexChanged.connect(self.__update_restart_label)
form_layout.addRow(Translations["settings.language"], self.language_combobox)
# Open Last Library on Start
self.open_last_lib_checkbox = QCheckBox()
self.open_last_lib_checkbox.setChecked(self.driver.settings.open_last_loaded_on_startup)
@@ -155,6 +150,54 @@ class SettingsPanel(PanelWidget):
Translations["settings.open_library_on_start"], self.open_last_lib_checkbox
)
# Generate Thumbnails
self.generate_thumbs = QCheckBox()
self.generate_thumbs.setChecked(self.driver.settings.generate_thumbs)
form_layout.addRow(Translations["settings.generate_thumbs"], self.generate_thumbs)
# Thumbnail Cache Size
self.thumb_cache_size_container = QWidget()
self.thumb_cache_size_layout = QHBoxLayout(self.thumb_cache_size_container)
self.thumb_cache_size_layout.setContentsMargins(0, 0, 0, 0)
self.thumb_cache_size_layout.setSpacing(6)
self.thumb_cache_size = QLineEdit()
self.thumb_cache_size.setAlignment(Qt.AlignmentFlag.AlignRight)
self.thumb_cache_size_validator = QDoubleValidator(
MIN_THUMB_CACHE_SIZE, 1_000_000_000, 2
) # High limit
self.thumb_cache_size.setValidator(self.thumb_cache_size_validator)
self.thumb_cache_size.setText(
str(max(self.driver.settings.thumb_cache_size, MIN_THUMB_CACHE_SIZE)).removesuffix(".0")
)
self.thumb_cache_size_layout.addWidget(self.thumb_cache_size)
self.thumb_cache_size_layout.setStretch(1, 2)
self.thumb_cache_size_layout.addWidget(QLabel("MiB"))
form_layout.addRow(
Translations["settings.thumb_cache_size.label"], self.thumb_cache_size_container
)
# Cached Thumbnail Resolution
self.cached_thumb_res_container = QWidget()
self.cached_thumb_res_layout = QHBoxLayout(self.cached_thumb_res_container)
self.cached_thumb_res_layout.setContentsMargins(0, 0, 0, 0)
self.cached_thumb_res_layout.setSpacing(6)
self.cached_thumb_res = QLineEdit()
self.cached_thumb_res.setAlignment(Qt.AlignmentFlag.AlignRight)
self.cached_thumb_res_validator = QIntValidator(MIN_CACHED_THUMB_RES, MAX_CACHED_THUMB_RES)
self.cached_thumb_res.setValidator(self.cached_thumb_res_validator)
self.cached_thumb_res.setText(str(self.driver.settings.cached_thumb_resolution))
self.cached_thumb_res_layout.addWidget(self.cached_thumb_res)
self.cached_thumb_res_layout.setStretch(1, 2)
self.cached_thumb_res_layout.addWidget(QLabel("px"))
form_layout.addRow(
Translations["settings.cached_thumb_resolution.label"], self.cached_thumb_res_container
)
# Autoplay
self.autoplay_checkbox = QCheckBox()
self.autoplay_checkbox.setChecked(self.driver.settings.autoplay)
form_layout.addRow(Translations["media_player.autoplay"], self.autoplay_checkbox)
# Scan for new files when a library is opened
self.scan_files_on_open_checkbox = QCheckBox()
self.scan_files_on_open_checkbox.setChecked(self.driver.settings.scan_files_on_open)
@@ -216,83 +259,26 @@ class SettingsPanel(PanelWidget):
Translations["settings.tag_click_action.label"], self.tag_click_action_combobox
)
# TODO: Implement Library Settings
def __build_library_settings(self): # pyright: ignore[reportUnusedFunction]
form_layout = QFormLayout(self.library_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
# Dark Mode
self.theme_combobox = QComboBox()
for k in SettingsPanel.theme_map:
self.theme_combobox.addItem(SettingsPanel.theme_map[k], k)
theme = self.driver.settings.theme
if theme not in SettingsPanel.theme_map:
theme = Theme.DEFAULT
self.theme_combobox.setCurrentIndex(list(SettingsPanel.theme_map.keys()).index(theme))
self.theme_combobox.currentIndexChanged.connect(self.__update_restart_label)
form_layout.addRow(Translations["settings.theme.label"], self.theme_combobox)
todo_label = QLabel("TODO")
form_layout.addRow(todo_label)
def __build_media_settings(self):
form_layout = QFormLayout(self.media_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
# Autoplay
self.autoplay_checkbox = QCheckBox()
self.autoplay_checkbox.setChecked(self.driver.settings.autoplay)
form_layout.addRow(Translations["media_player.autoplay"], self.autoplay_checkbox)
# Generate Thumbnails
self.generate_thumbs = QCheckBox()
self.generate_thumbs.setChecked(self.driver.settings.generate_thumbs)
form_layout.addRow(Translations["settings.generate_thumbs"], self.generate_thumbs)
# Thumbnail Cache Size
self.thumb_cache_size_container = QWidget()
self.thumb_cache_size_layout = QHBoxLayout(self.thumb_cache_size_container)
self.thumb_cache_size_layout.setContentsMargins(0, 0, 0, 0)
self.thumb_cache_size_layout.setSpacing(6)
self.thumb_cache_size = QLineEdit()
self.thumb_cache_size.setAlignment(Qt.AlignmentFlag.AlignRight)
self.thumb_cache_size_validator = QDoubleValidator(
MIN_THUMB_CACHE_SIZE, 1_000_000_000, 2
) # High limit
self.thumb_cache_size.setValidator(self.thumb_cache_size_validator)
self.thumb_cache_size.setText(
str(max(self.driver.settings.thumb_cache_size, MIN_THUMB_CACHE_SIZE)).removesuffix(".0")
)
self.thumb_cache_size_layout.addWidget(self.thumb_cache_size)
self.thumb_cache_size_layout.setStretch(1, 2)
self.thumb_cache_size_layout.addWidget(QLabel("MiB"))
form_layout.addRow(
Translations["settings.thumb_cache_size.label"], self.thumb_cache_size_container
)
# Cached Thumbnail Resolution
self.cached_thumb_res_container = QWidget()
self.cached_thumb_res_layout = QHBoxLayout(self.cached_thumb_res_container)
self.cached_thumb_res_layout.setContentsMargins(0, 0, 0, 0)
self.cached_thumb_res_layout.setSpacing(6)
self.cached_thumb_res = QLineEdit()
self.cached_thumb_res.setAlignment(Qt.AlignmentFlag.AlignRight)
self.cached_thumb_res_validator = QIntValidator(MIN_CACHED_THUMB_RES, MAX_CACHED_THUMB_RES)
self.cached_thumb_res.setValidator(self.cached_thumb_res_validator)
self.cached_thumb_res.setText(str(self.driver.settings.cached_thumb_resolution))
self.cached_thumb_res_layout.addWidget(self.cached_thumb_res)
self.cached_thumb_res_layout.setStretch(1, 2)
self.cached_thumb_res_layout.addWidget(QLabel("px"))
form_layout.addRow(
Translations["settings.cached_thumb_resolution.label"], self.cached_thumb_res_container
)
def __build_localization_settings(self):
form_layout = QFormLayout(self.localization_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
# Language
self.language_combobox = QComboBox()
translated_langs = [(Translations[f"language.{lang}"], lang) for lang in LANGUAGES]
translated_langs.sort(key=lambda x: x[0])
for lang in translated_langs:
self.language_combobox.addItem(lang[0], lang[1])
current_lang: str = self.driver.settings.language
if current_lang not in LANGUAGES:
current_lang = DEFAULT_TRANSLATION
self.language_combobox.setCurrentIndex([x[1] for x in translated_langs].index(current_lang))
self.language_combobox.currentIndexChanged.connect(self.__update_restart_label)
form_layout.addRow(Translations["settings.language"], self.language_combobox)
# Splash Screen
self.splash_combobox = QComboBox()
for k in SettingsPanel.splash_map:
self.splash_combobox.addItem(SettingsPanel.splash_map[k], k)
splash = self.driver.settings.splash
if splash not in SettingsPanel.splash_map:
splash = Splash.DEFAULT
self.splash_combobox.setCurrentIndex(list(SettingsPanel.splash_map.keys()).index(splash))
form_layout.addRow(Translations["settings.splash.label"], self.splash_combobox)
# Date Format
self.dateformat_combobox = QComboBox()
@@ -317,34 +303,20 @@ class SettingsPanel(PanelWidget):
self.zeropadding_checkbox.setChecked(self.driver.settings.zero_padding)
form_layout.addRow(Translations["settings.zeropadding.label"], self.zeropadding_checkbox)
def __build_appearance_settings(self):
form_layout = QFormLayout(self.appearance_settings_container)
# TODO: Implement Library Settings
def __build_library_settings(self): # pyright: ignore[reportUnusedFunction]
form_layout = QFormLayout(self.library_settings_container)
form_layout.setContentsMargins(6, 6, 6, 6)
# Dark Mode
self.theme_combobox = QComboBox()
for k in SettingsPanel.theme_map:
self.theme_combobox.addItem(SettingsPanel.theme_map[k], k)
theme = self.driver.settings.theme
if theme not in SettingsPanel.theme_map:
theme = Theme.DEFAULT
self.theme_combobox.setCurrentIndex(list(SettingsPanel.theme_map.keys()).index(theme))
self.theme_combobox.currentIndexChanged.connect(self.__update_restart_label)
form_layout.addRow(Translations["settings.theme.label"], self.theme_combobox)
todo_label = QLabel("TODO")
form_layout.addRow(todo_label)
# Splash Screen
self.splash_combobox = QComboBox()
for k in SettingsPanel.splash_map:
self.splash_combobox.addItem(SettingsPanel.splash_map[k], k)
splash = self.driver.settings.splash
if splash not in SettingsPanel.splash_map:
splash = Splash.DEFAULT
self.splash_combobox.setCurrentIndex(list(SettingsPanel.splash_map.keys()).index(splash))
form_layout.addRow(Translations["settings.splash.label"], self.splash_combobox)
def __get_language(self) -> str:
return list(LANGUAGES.values())[self.language_combobox.currentIndex()]
def get_settings(self) -> dict[str, Any]: # pyright: ignore[reportExplicitAny]
return {
"language": self.language_combobox.currentData(),
"language": self.__get_language(),
"open_last_loaded_on_startup": self.open_last_lib_checkbox.isChecked(),
"generate_thumbs": self.generate_thumbs.isChecked(),
"thumb_cache_size": max(
+32 -32
View File
@@ -16,38 +16,38 @@ logger = structlog.get_logger(__name__)
DEFAULT_TRANSLATION = "en"
LANGUAGES = [
# "am", # Minimal
"ceb",
"cs",
# "da", # Minimal
"de",
"el",
"en",
"es",
"fi",
"fil",
"fr",
"hu",
# "is", # Minimal
"it",
"ja",
"nb_NO",
"nl",
"pl",
"pt_BR",
"pt",
"qpv",
"ro",
"ru",
"sv",
"ta",
# "th", # Minimal
"tok",
"tr",
"zh_Hans",
"zh_Hant",
]
LANGUAGES = {
# "Amharic": "am", # Minimal
"Cebuano": "ceb",
"Chinese (Simplified)": "zh_Hans",
"Chinese (Traditional)": "zh_Hant",
"Czech": "cs",
# "Danish": "da", # Minimal
"Dutch": "nl",
"English": "en",
"Filipino": "fil",
"Finnish": "fi",
"French": "fr",
"German": "de",
"Greek": "el",
"Hungarian": "hu",
# "Icelandic": "is", # Minimal
"Italian": "it",
"Japanese": "ja",
"Norwegian Bokmål": "nb_NO",
"Polish": "pl",
"Portuguese (Brazil)": "pt_BR",
"Portuguese (Portugal)": "pt",
"Romanian": "ro",
"Russian": "ru",
"Spanish": "es",
"Swedish": "sv",
"Tamil": "ta",
# "Thai": "th", # Minimal
"Toki Pona": "tok",
"Turkish": "tr",
"Viossa": "qpv",
}
# A map of field class names to their respective translation keys.
FIELD_TYPE_KEYS = {
+12 -11
View File
@@ -59,7 +59,7 @@ from tagstudio.qt.controllers.field_template_search_panel_controller import Fiel
from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntriesModal
from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal
from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal, TagSearchPanel
from tagstudio.qt.controllers.update_available_message_box import UpdateAvailableMessageBox
from tagstudio.qt.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme
from tagstudio.qt.mixed.about_modal import AboutModal
@@ -85,6 +85,7 @@ from tagstudio.qt.views.main_window import MainWindow
from tagstudio.qt.views.panel_modal import PanelModal
from tagstudio.qt.views.splash import SplashScreen
from tagstudio.qt.views.stylesheets.stylesheets import header
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
BADGE_TAGS = {
BadgeType.FAVORITE: TAG_FAVORITE,
@@ -351,13 +352,15 @@ class QtDriver(DriverMixin, QObject):
self.app.setDesktopFileName("tagstudio")
# Initialize the Tag Manager panel
self.tag_manager_panel = TagSearchModal(
self.lib,
self.tag_manager_panel = PanelModal(
widget=TagSearchPanel(
self.lib,
is_tag_chooser=False,
view=TagSearchPanelView(is_tag_chooser=False),
),
title=Translations["tag_manager.title"],
is_tag_chooser=False,
is_savable=False,
)
self.tag_manager_panel.tsp.set_driver(self)
self.tag_manager_panel.done.connect(
lambda checked=False: self.main_window.preview_panel.set_selection(
self.selected, update_preview=False
@@ -383,10 +386,8 @@ class QtDriver(DriverMixin, QObject):
)
)
# Initialize the "Add Tag" panel
self.add_tag_modal = TagSearchModal(
self.lib, title=Translations["tag.add.plural"], is_tag_chooser=True
)
# Initialize the Tag Search panel
self.add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
self.add_tag_modal.tsp.set_driver(self)
self.add_tag_modal.tsp.item_chosen.connect(
lambda chosen_tag: (
@@ -858,7 +859,7 @@ class QtDriver(DriverMixin, QObject):
self.modal = PanelModal(
panel,
Translations["tag.new"],
Translations["tag.create"],
Translations["tag.add"],
is_savable=True,
)
@@ -196,36 +196,6 @@
"json_migration.title.new_lib": "<h2>v9.5+ Library</h2>",
"json_migration.title.old_lib": "<h2>v9.4 Library</h2>",
"landing.open_create_library": "Open/Create Library {shortcut}",
"language.am": "Amharic",
"language.ceb": "Cebuano",
"language.cs": "Czech",
"language.da": "Danish",
"language.de": "German",
"language.el": "Greek",
"language.en": "English",
"language.es": "Spanish",
"language.fi": "Finnish",
"language.fil": "Filipino",
"language.fr": "French",
"language.hu": "Hungarian",
"language.is": "Icelandic",
"language.it": "Italian",
"language.ja": "Japanese",
"language.nb_NO": "Norwegian Bokmål",
"language.nl": "Dutch",
"language.pl": "Polish",
"language.pt": "Portuguese",
"language.pt_BR": "Portuguese (Brazil)",
"language.qpv": "Viossa",
"language.ro": "Romanian",
"language.ru": "Russian",
"language.sv": "Swedish",
"language.ta": "Tamil",
"language.th": "Thai",
"language.tok": "Toki Pona",
"language.tr": "Turkish",
"language.zh_Hans": "Chinese (Simplified)",
"language.zh_Hant": "Chinese (Traditional)",
"library_info.cleanup": "Cleanup",
"library_info.cleanup.backups": "Library Backups:",
"library_info.cleanup.dupe_files": "Duplicate Files:",
@@ -307,7 +277,6 @@
"select.all": "Select All",
"select.clear": "Clear Selection",
"select.inverse": "Invert Selection",
"settings.appearance": "Appearance",
"settings.cached_thumb_resolution.label": "Cached Thumbnail Resolution",
"settings.clear_thumb_cache.title": "Clear Thumbnail Cache",
"settings.dateformat.english": "English",
@@ -324,8 +293,6 @@
"settings.infinite_scroll": "Infinite Scrolling",
"settings.language": "Language",
"settings.library": "Library Settings",
"settings.localization": "Localization",
"settings.media": "Media",
"settings.open_library_on_start": "Open Library on Start",
"settings.page_size": "Page Size",
"settings.restart_required": "Please restart TagStudio for changes to take effect.",
Binary file not shown.
+4 -4
View File
@@ -81,9 +81,9 @@ def test_library_add_file(library: Library):
fields=[TextField(name="Title", value="I'm a Test Title")],
)
assert not library.has_entry_with_path(entry.path)
assert not library.get_entry_id_from_path(entry.path)
assert library.add_entries([entry])
assert library.has_entry_with_path(entry.path)
assert library.get_entry_id_from_path(entry.path)
def test_create_tag(library: Library, generate_tag: Callable[..., Tag]):
@@ -347,8 +347,8 @@ def test_merge_entries(library: Library):
entry_b_: Entry = unwrap(library.get_entry_full(entry_b_id))
assert library.merge_entries(entry_a_, entry_b_)
assert not library.has_entry_with_path(Path("a"))
assert library.has_entry_with_path(Path("b"))
assert not library.get_entry_id_from_path(Path("a"))
assert library.get_entry_id_from_path(Path("b"))
entry_b_merged = unwrap(library.get_entry_full(entry_b_id))