mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-07-18 19:46:19 +02:00
Compare commits
8 Commits
v9.5.7
...
2bcbdd035e
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bcbdd035e | |||
| 7af7420167 | |||
| cece7920a8 | |||
| ad2cbbca48 | |||
| 96fc5ef065 | |||
| b0decac610 | |||
| 910d2b735d | |||
| 47d4de5825 |
+10
-2
@@ -128,7 +128,15 @@ Migration from the legacy JSON format is provided via a walkthrough when opening
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [#1139](https://github.com/TagStudioDev/TagStudio/pull/1139) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
| [#1139](https://github.com/TagStudioDev/TagStudio/pull/1139) | 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.
|
||||
- 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
|
||||
|
||||
| Used From | Format | Location |
|
||||
| ----------------------------------------------------------------------- | ------ | ----------------------------------------------- |
|
||||
| [#1298](https://github.com/TagStudioDev/TagStudio/pull/1298) | SQLite | `<Library Folder>`/.TagStudio/ts_library.sqlite |
|
||||
|
||||
- Removes the `preferences` table, after migrating the contained extension list to the .ts_ignore file, if necessary.
|
||||
|
||||
+2
-1
@@ -24,7 +24,7 @@ dependencies = [
|
||||
"pydub~=0.25",
|
||||
"PySide6==6.8.0.*",
|
||||
"rarfile==4.2",
|
||||
"rawpy~=0.24",
|
||||
"rawpy~=0.27",
|
||||
"Send2Trash~=1.8",
|
||||
"SQLAlchemy~=2.0",
|
||||
"srctools~=2.6",
|
||||
@@ -46,6 +46,7 @@ pyinstaller = ["Pyinstaller~=6.13"]
|
||||
pytest = [
|
||||
"pytest==8.3.5",
|
||||
"pytest-cov==6.1.1",
|
||||
"pytest-mock==3.15.1",
|
||||
"pytest-qt==4.4.0",
|
||||
"syrupy==4.9.1",
|
||||
]
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import enum
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class SettingItems(str, enum.Enum):
|
||||
@@ -57,30 +55,3 @@ class MacroID(enum.Enum):
|
||||
BUILD_URL = "build_url"
|
||||
MATCH = "match"
|
||||
CLEAN_URL = "clean_url"
|
||||
|
||||
|
||||
class DefaultEnum(enum.Enum):
|
||||
"""Allow saving multiple identical values in property called .default."""
|
||||
|
||||
default: Any
|
||||
|
||||
def __new__(cls, value):
|
||||
# Create the enum instance
|
||||
obj = object.__new__(cls)
|
||||
# make value random
|
||||
obj._value_ = uuid4()
|
||||
# assign the actual value into .default property
|
||||
obj.default = value
|
||||
return obj
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
raise AttributeError("access the value via .default property instead")
|
||||
|
||||
|
||||
# TODO: Remove DefaultEnum and LibraryPrefs classes once remaining values are removed.
|
||||
class LibraryPrefs(DefaultEnum):
|
||||
"""Library preferences with default value accessible via .default property."""
|
||||
|
||||
IS_EXCLUDE_LIST = True
|
||||
EXTENSION_LIST = [".json", ".xmp", ".aae"]
|
||||
|
||||
@@ -8,10 +8,9 @@ from sqlalchemy import text
|
||||
SQL_FILENAME: str = "ts_library.sqlite"
|
||||
JSON_FILENAME: str = "ts_library.json"
|
||||
|
||||
DB_VERSION_LEGACY_KEY: str = "DB_VERSION"
|
||||
DB_VERSION_CURRENT_KEY: str = "CURRENT"
|
||||
DB_VERSION_INITIAL_KEY: str = "INITIAL"
|
||||
DB_VERSION: int = 103
|
||||
DB_VERSION: int = 104
|
||||
|
||||
TAG_CHILDREN_QUERY = text("""
|
||||
WITH RECURSIVE ChildTags AS (
|
||||
|
||||
@@ -16,7 +16,7 @@ from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from os import makedirs
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import uuid4
|
||||
from warnings import catch_warnings
|
||||
|
||||
@@ -53,7 +53,6 @@ from sqlalchemy.orm import (
|
||||
noload,
|
||||
selectinload,
|
||||
)
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from tagstudio.core.constants import (
|
||||
BACKUP_FOLDER_NAME,
|
||||
@@ -67,13 +66,11 @@ from tagstudio.core.constants import (
|
||||
TAG_META,
|
||||
TS_FOLDER_NAME,
|
||||
)
|
||||
from tagstudio.core.enums import LibraryPrefs
|
||||
from tagstudio.core.library.alchemy import default_color_groups
|
||||
from tagstudio.core.library.alchemy.constants import (
|
||||
DB_VERSION,
|
||||
DB_VERSION_CURRENT_KEY,
|
||||
DB_VERSION_INITIAL_KEY,
|
||||
DB_VERSION_LEGACY_KEY,
|
||||
JSON_FILENAME,
|
||||
SQL_FILENAME,
|
||||
TAG_CHILDREN_QUERY,
|
||||
@@ -96,7 +93,6 @@ from tagstudio.core.library.alchemy.models import (
|
||||
Entry,
|
||||
Folder,
|
||||
Namespace,
|
||||
Preferences,
|
||||
Tag,
|
||||
TagAlias,
|
||||
TagColorGroup,
|
||||
@@ -104,6 +100,7 @@ from tagstudio.core.library.alchemy.models import (
|
||||
Version,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.visitors import SQLBoolExpressionBuilder
|
||||
from tagstudio.core.library.ignore import migrate_ext_list
|
||||
from tagstudio.core.library.json.library import Library as JsonLibrary
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.translations import Translations
|
||||
@@ -218,7 +215,6 @@ class Library:
|
||||
"""Class for the Library object, and all CRUD operations made upon it."""
|
||||
|
||||
library_dir: Path | None = None
|
||||
storage_path: Path | str | None = None
|
||||
engine: Engine | None = None
|
||||
folder: Folder | None = None
|
||||
included_files: set[Path] = set()
|
||||
@@ -233,7 +229,6 @@ class Library:
|
||||
if self.engine:
|
||||
self.engine.dispose()
|
||||
self.library_dir = None
|
||||
self.storage_path = None
|
||||
self.folder = None
|
||||
self.included_files = set()
|
||||
|
||||
@@ -320,9 +315,10 @@ class Library:
|
||||
value=v,
|
||||
)
|
||||
|
||||
# Preferences
|
||||
self.set_prefs(LibraryPrefs.EXTENSION_LIST, [x.strip(".") for x in json_lib.ext_list])
|
||||
self.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, json_lib.is_exclude_list)
|
||||
# extension include/exclude list
|
||||
(unwrap(self.library_dir) / TS_FOLDER_NAME / IGNORE_NAME).write_text(
|
||||
migrate_ext_list([x.strip(".") for x in json_lib.ext_list], json_lib.is_exclude_list)
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
logger.info(f"Library Converted! ({format_timespan(end_time - start_time)})")
|
||||
@@ -349,33 +345,36 @@ class Library:
|
||||
else:
|
||||
return tag.name
|
||||
|
||||
def open_library(
|
||||
self, library_dir: Path, storage_path: Path | str | None = None
|
||||
def open_library(self, library_dir: Path, in_memory: bool = False) -> LibraryStatus:
|
||||
"""Wrapper for open_sqlite_library.
|
||||
|
||||
Handles in-memory storage and checks whether a JSON-migration is necessary.
|
||||
"""
|
||||
assert isinstance(library_dir, Path)
|
||||
|
||||
if in_memory:
|
||||
return self.open_sqlite_library(library_dir, is_new=True, storage_path=":memory:")
|
||||
|
||||
is_new = True
|
||||
sql_path = library_dir / TS_FOLDER_NAME / SQL_FILENAME
|
||||
if self.verify_ts_folder(library_dir) and (is_new := not sql_path.exists()):
|
||||
json_path = library_dir / TS_FOLDER_NAME / JSON_FILENAME
|
||||
if json_path.exists():
|
||||
return LibraryStatus(
|
||||
success=False,
|
||||
library_path=library_dir,
|
||||
message="[JSON] Legacy v9.4 library requires conversion to v9.5+",
|
||||
json_migration_req=True,
|
||||
)
|
||||
|
||||
return self.open_sqlite_library(library_dir, is_new, str(sql_path))
|
||||
|
||||
def open_sqlite_library(
|
||||
self, library_dir: Path, is_new: bool, storage_path: str
|
||||
) -> LibraryStatus:
|
||||
is_new: bool = True
|
||||
if storage_path == ":memory:":
|
||||
self.storage_path = storage_path
|
||||
is_new = True
|
||||
return self.open_sqlite_library(library_dir, is_new)
|
||||
else:
|
||||
self.storage_path = library_dir / TS_FOLDER_NAME / SQL_FILENAME
|
||||
assert isinstance(self.storage_path, Path)
|
||||
if self.verify_ts_folder(library_dir) and (is_new := not self.storage_path.exists()):
|
||||
json_path = library_dir / TS_FOLDER_NAME / JSON_FILENAME
|
||||
if json_path.exists():
|
||||
return LibraryStatus(
|
||||
success=False,
|
||||
library_path=library_dir,
|
||||
message="[JSON] Legacy v9.4 library requires conversion to v9.5+",
|
||||
json_migration_req=True,
|
||||
)
|
||||
|
||||
return self.open_sqlite_library(library_dir, is_new)
|
||||
|
||||
def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus:
|
||||
connection_string = URL.create(
|
||||
drivername="sqlite",
|
||||
database=str(self.storage_path),
|
||||
database=storage_path,
|
||||
)
|
||||
# NOTE: File-based databases should use NullPool to create new DB connection in order to
|
||||
# keep connections on separate threads, which prevents the DB files from being locked
|
||||
@@ -384,7 +383,7 @@ class Library:
|
||||
# More info can be found on the SQLAlchemy docs:
|
||||
# https://docs.sqlalchemy.org/en/20/changelog/migration_07.html
|
||||
# Under -> sqlite-the-sqlite-dialect-now-uses-nullpool-for-file-based-databases
|
||||
poolclass = None if self.storage_path == ":memory:" else NullPool
|
||||
poolclass = None if storage_path == ":memory:" else NullPool
|
||||
loaded_db_version: int = 0
|
||||
|
||||
logger.info(
|
||||
@@ -422,8 +421,8 @@ class Library:
|
||||
logger.info(f"[Library] DB_VERSION: {loaded_db_version}")
|
||||
make_tables(self.engine)
|
||||
|
||||
# Add default tag color namespaces.
|
||||
if is_new:
|
||||
# Add default tag color namespaces.
|
||||
namespaces = default_color_groups.namespaces()
|
||||
try:
|
||||
session.add_all(namespaces)
|
||||
@@ -432,8 +431,7 @@ class Library:
|
||||
logger.error("[Library] Couldn't add default tag color namespaces", error=e)
|
||||
session.rollback()
|
||||
|
||||
# Add default tag colors.
|
||||
if is_new:
|
||||
# Add default tag colors.
|
||||
tag_colors: list[TagColorGroup] = default_color_groups.standard()
|
||||
tag_colors += default_color_groups.pastels()
|
||||
tag_colors += default_color_groups.shades()
|
||||
@@ -448,8 +446,7 @@ class Library:
|
||||
logger.error("[Library] Couldn't add default tag colors", error=e)
|
||||
session.rollback()
|
||||
|
||||
# Add default tags.
|
||||
if is_new:
|
||||
# Add default tags.
|
||||
tags = get_default_tags()
|
||||
try:
|
||||
session.add_all(tags)
|
||||
@@ -459,15 +456,6 @@ class Library:
|
||||
|
||||
# Ensure version rows are present
|
||||
with catch_warnings(record=True):
|
||||
# NOTE: The "Preferences" table is depreciated and will be removed in the future.
|
||||
# The DB_VERSION is still being set to it in order to remain backwards-compatible
|
||||
# with existing TagStudio versions until it is removed.
|
||||
try:
|
||||
session.add(Preferences(key=DB_VERSION_LEGACY_KEY, value=DB_VERSION))
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
|
||||
try:
|
||||
initial = DB_VERSION if is_new else 100
|
||||
session.add(Version(key=DB_VERSION_INITIAL_KEY, value=initial))
|
||||
@@ -481,15 +469,6 @@ class Library:
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
|
||||
# TODO: Remove this "Preferences" system.
|
||||
for pref in LibraryPrefs:
|
||||
with catch_warnings(record=True):
|
||||
try:
|
||||
session.add(Preferences(key=pref.name, value=pref.default))
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
session.rollback()
|
||||
|
||||
for field in FieldID:
|
||||
try:
|
||||
session.add(
|
||||
@@ -530,36 +509,36 @@ class Library:
|
||||
|
||||
# Apply any post-SQL migration patches.
|
||||
if not is_new:
|
||||
assert loaded_db_version >= 6
|
||||
|
||||
# save backup if patches will be applied
|
||||
if loaded_db_version < DB_VERSION:
|
||||
self.library_dir = library_dir
|
||||
self.save_library_backup_to_disk()
|
||||
self.library_dir = None
|
||||
|
||||
# NOTE: Depending on the data, some data and schema changes need to be applied in
|
||||
# different orders. This chain of methods can likely be cleaned up and/or moved.
|
||||
# migrate DB step by step from one version to the next
|
||||
if loaded_db_version < 7:
|
||||
# changes: value_type, tags
|
||||
self.__apply_db7_migration(session)
|
||||
if loaded_db_version < 8:
|
||||
self.__apply_db8_schema_changes(session)
|
||||
# changes: tag_colors
|
||||
self.__apply_db8_migration(session)
|
||||
if loaded_db_version < 9:
|
||||
self.__apply_db9_schema_changes(session)
|
||||
if loaded_db_version < 103:
|
||||
self.__apply_db103_schema_changes(session)
|
||||
if loaded_db_version == 6:
|
||||
self.__apply_repairs_for_db6(session)
|
||||
|
||||
if loaded_db_version >= 6 and loaded_db_version < 8:
|
||||
self.__apply_db8_default_data(session)
|
||||
if loaded_db_version < 9:
|
||||
self.__apply_db9_filename_population(session)
|
||||
# changes: entries
|
||||
self.__apply_db9_migration(session)
|
||||
if loaded_db_version < 100:
|
||||
self.__apply_db100_parent_repairs(session)
|
||||
# changes: tag_parents
|
||||
self.__apply_db100_migration(session)
|
||||
if loaded_db_version < 102:
|
||||
self.__apply_db102_repairs(session)
|
||||
# changes: tag_parents
|
||||
self.__apply_db102_migration(session)
|
||||
if loaded_db_version < 103:
|
||||
self.__apply_db103_default_data(session)
|
||||
|
||||
# Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist
|
||||
self.migrate_sql_to_ts_ignore(library_dir)
|
||||
# changes: tags
|
||||
self.__apply_db103_migration(session)
|
||||
if loaded_db_version < 104:
|
||||
# changes: deletes preferences
|
||||
self.__apply_db104_migrations(session, library_dir)
|
||||
|
||||
# Update DB_VERSION
|
||||
if loaded_db_version < DB_VERSION:
|
||||
@@ -569,8 +548,8 @@ class Library:
|
||||
self.library_dir = library_dir
|
||||
return LibraryStatus(success=True, library_path=library_dir)
|
||||
|
||||
def __apply_repairs_for_db6(self, session: Session):
|
||||
"""Apply database repairs introduced in DB_VERSION 7."""
|
||||
def __apply_db7_migration(self, session: Session):
|
||||
"""Migrate DB from DB_VERSION 6 to 7."""
|
||||
logger.info("[Library][Migration] Applying patches to DB_VERSION: 6 library...")
|
||||
with session:
|
||||
# Repair "Description" fields with a TEXT_LINE key instead of a TEXT_BOX key.
|
||||
@@ -583,7 +562,7 @@ class Library:
|
||||
session.flush()
|
||||
|
||||
# Repair tags that may have a disambiguation_id pointing towards a deleted tag.
|
||||
all_tag_ids: set[int] = {tag.id for tag in self.tags}
|
||||
all_tag_ids = session.scalars(text("SELECT DISTINCT id FROM tags")).all()
|
||||
disam_stmt = (
|
||||
update(Tag)
|
||||
.where(Tag.disambiguation_id.not_in(all_tag_ids))
|
||||
@@ -592,9 +571,8 @@ class Library:
|
||||
session.execute(disam_stmt)
|
||||
session.commit()
|
||||
|
||||
def __apply_db8_schema_changes(self, session: Session):
|
||||
"""Apply database schema changes introduced in DB_VERSION 8."""
|
||||
# TODO: Use Alembic for this part instead
|
||||
def __apply_db8_migration(self, session: Session):
|
||||
"""Migrate DB from DB_VERSION 7 to 8."""
|
||||
# Add the missing color_border column to the TagColorGroups table.
|
||||
color_border_stmt = text(
|
||||
"ALTER TABLE tag_colors ADD COLUMN color_border BOOLEAN DEFAULT FALSE NOT NULL"
|
||||
@@ -610,8 +588,7 @@ class Library:
|
||||
)
|
||||
session.rollback()
|
||||
|
||||
def __apply_db8_default_data(self, session: Session):
|
||||
"""Apply default data changes introduced in DB_VERSION 8."""
|
||||
# collect new default tag colors
|
||||
tag_colors: list[TagColorGroup] = default_color_groups.standard()
|
||||
tag_colors += default_color_groups.pastels()
|
||||
tag_colors += default_color_groups.shades()
|
||||
@@ -660,8 +637,9 @@ class Library:
|
||||
)
|
||||
session.rollback()
|
||||
|
||||
def __apply_db9_schema_changes(self, session: Session):
|
||||
"""Apply database schema changes introduced in DB_VERSION 9."""
|
||||
def __apply_db9_migration(self, session: Session):
|
||||
"""Migrate DB from DB_VERSION 8 to 9."""
|
||||
# Apply database schema changes
|
||||
add_filename_column = text(
|
||||
"ALTER TABLE entries ADD COLUMN filename TEXT NOT NULL DEFAULT ''"
|
||||
)
|
||||
@@ -676,15 +654,14 @@ class Library:
|
||||
)
|
||||
session.rollback()
|
||||
|
||||
def __apply_db9_filename_population(self, session: Session):
|
||||
"""Populate the filename column introduced in DB_VERSION 9."""
|
||||
# Populate the new filename column.
|
||||
for entry in self.all_entries():
|
||||
session.merge(entry).filename = entry.path.name
|
||||
session.commit()
|
||||
logger.info("[Library][Migration] Populated filename column in entries table")
|
||||
|
||||
def __apply_db100_parent_repairs(self, session: Session):
|
||||
"""Swap the child_id and parent_id values in the TagParent table."""
|
||||
def __apply_db100_migration(self, session: Session):
|
||||
"""Migrate DB to DB_VERSION 100."""
|
||||
with session:
|
||||
# Repair parent-child tag relationships that are the wrong way around.
|
||||
stmt = update(TagParent).values(
|
||||
@@ -695,17 +672,18 @@ class Library:
|
||||
session.commit()
|
||||
logger.info("[Library][Migration] Refactored TagParent table")
|
||||
|
||||
def __apply_db102_repairs(self, session: Session):
|
||||
"""Repair tag_parents rows with references to deleted tags."""
|
||||
def __apply_db102_migration(self, session: Session):
|
||||
"""Migrate DB to DB_VERSION 102."""
|
||||
with session:
|
||||
all_tag_ids: list[int] = [t.id for t in self.tags]
|
||||
all_tag_ids = session.scalars(text("SELECT DISTINCT id FROM tags")).all()
|
||||
stmt = delete(TagParent).where(TagParent.parent_id.not_in(all_tag_ids))
|
||||
session.execute(stmt)
|
||||
session.commit()
|
||||
logger.info("[Library][Migration] Verified TagParent table data")
|
||||
|
||||
def __apply_db103_schema_changes(self, session: Session):
|
||||
"""Apply database schema changes introduced in DB_VERSION 103."""
|
||||
def __apply_db103_migration(self, session: Session):
|
||||
"""Migrate DB from DB_VERSION 102 to 103."""
|
||||
# add the new hidden column for tags
|
||||
add_is_hidden_column = text(
|
||||
"ALTER TABLE tags ADD COLUMN is_hidden BOOLEAN NOT NULL DEFAULT 0"
|
||||
)
|
||||
@@ -720,8 +698,7 @@ class Library:
|
||||
)
|
||||
session.rollback()
|
||||
|
||||
def __apply_db103_default_data(self, session: Session):
|
||||
"""Apply default data changes introduced in DB_VERSION 103."""
|
||||
# mark the "Archived" tag as hidden
|
||||
try:
|
||||
session.query(Tag).filter(Tag.id == TAG_ARCHIVED).update({"is_hidden": True})
|
||||
session.commit()
|
||||
@@ -734,33 +711,30 @@ class Library:
|
||||
)
|
||||
session.rollback()
|
||||
|
||||
def migrate_sql_to_ts_ignore(self, library_dir: Path):
|
||||
def __apply_db104_migrations(self, session: Session, library_dir: Path):
|
||||
"""Migrate DB from DB_VERSION 103 to 104."""
|
||||
# Convert file extension list to ts_ignore file, if a .ts_ignore file does not exist
|
||||
self.__migrate_sql_to_ts_ignore(library_dir)
|
||||
session.execute(text("DROP TABLE preferences"))
|
||||
session.commit()
|
||||
|
||||
def __migrate_sql_to_ts_ignore(self, library_dir: Path):
|
||||
# Do not continue if existing '.ts_ignore' file is found
|
||||
if Path(library_dir / TS_FOLDER_NAME / IGNORE_NAME).exists():
|
||||
ts_ignore = library_dir / TS_FOLDER_NAME / IGNORE_NAME
|
||||
if Path(ts_ignore).exists():
|
||||
return
|
||||
|
||||
# Create blank '.ts_ignore' file
|
||||
ts_ignore_template = (
|
||||
Path(__file__).parents[3] / "resources/templates/ts_ignore_template_blank.txt"
|
||||
)
|
||||
ts_ignore = library_dir / TS_FOLDER_NAME / IGNORE_NAME
|
||||
try:
|
||||
shutil.copy2(ts_ignore_template, ts_ignore)
|
||||
except Exception as e:
|
||||
logger.error("[ERROR][Library] Could not generate '.ts_ignore' file!", error=e)
|
||||
|
||||
# Load legacy extension data
|
||||
extensions: list[str] = self.prefs(LibraryPrefs.EXTENSION_LIST) # pyright: ignore
|
||||
is_exclude_list: bool = self.prefs(LibraryPrefs.IS_EXCLUDE_LIST) # pyright: ignore
|
||||
with Session(self.engine) as session:
|
||||
extensions: list[str] = unwrap(
|
||||
session.scalar(text("SELECT value FROM preferences WHERE key = 'EXTENSION_LIST'"))
|
||||
)
|
||||
is_exclude_list: bool = unwrap(
|
||||
session.scalar(text("SELECT value FROM preferences WHERE key = 'IS_EXCLUDE_LIST'"))
|
||||
)
|
||||
|
||||
# Copy extensions to '.ts_ignore' file
|
||||
if ts_ignore.exists():
|
||||
with open(ts_ignore, "a") as f:
|
||||
prefix = ""
|
||||
if not is_exclude_list:
|
||||
prefix = "!"
|
||||
f.write("*\n")
|
||||
f.writelines([f"{prefix}*.{x.lstrip('.')}\n" for x in extensions])
|
||||
with open(ts_ignore, "w") as f:
|
||||
f.write(migrate_ext_list(extensions, is_exclude_list))
|
||||
|
||||
@property
|
||||
def default_fields(self) -> list[BaseField]:
|
||||
@@ -1858,19 +1832,20 @@ class Library:
|
||||
engine = sqlalchemy.inspect(self.engine)
|
||||
try:
|
||||
# "Version" table added in DB_VERSION 101
|
||||
if engine and engine.has_table("Version"):
|
||||
if engine and engine.has_table("versions"):
|
||||
version = session.scalar(select(Version).where(Version.key == key))
|
||||
assert version
|
||||
return version.value
|
||||
# NOTE: The "Preferences" table has been depreciated as of TagStudio 9.5.4
|
||||
# and is set to be removed in a future release.
|
||||
else:
|
||||
pref_version = session.scalar(
|
||||
select(Preferences).where(Preferences.key == DB_VERSION_LEGACY_KEY)
|
||||
return int(
|
||||
unwrap(
|
||||
session.scalar(
|
||||
text("SELECT value FROM preferences WHERE key == 'DB_VERSION'")
|
||||
)
|
||||
)
|
||||
)
|
||||
assert pref_version
|
||||
assert isinstance(pref_version.value, int)
|
||||
return pref_version.value
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
@@ -1888,60 +1863,10 @@ class Library:
|
||||
version.value = value
|
||||
session.add(version)
|
||||
session.commit()
|
||||
|
||||
# If a depreciated "Preferences" table is found, update the version value to be read
|
||||
# by older TagStudio versions.
|
||||
engine = sqlalchemy.inspect(self.engine)
|
||||
if engine and engine.has_table("Preferences"):
|
||||
pref = unwrap(
|
||||
session.scalar(
|
||||
select(Preferences).where(Preferences.key == DB_VERSION_LEGACY_KEY)
|
||||
)
|
||||
)
|
||||
pref.value = value # pyright: ignore
|
||||
session.add(pref)
|
||||
session.commit()
|
||||
except (IntegrityError, AssertionError) as e:
|
||||
logger.error("[Library][ERROR] Couldn't add default tag color namespaces", error=e)
|
||||
session.rollback()
|
||||
|
||||
# TODO: Remove this once the 'preferences' table is removed.
|
||||
@deprecated("Use `get_version() for version and `ts_ignore` system for extension exclusion.")
|
||||
def prefs(self, key: str | LibraryPrefs): # pyright: ignore[reportUnknownParameterType]
|
||||
# load given item from Preferences table
|
||||
with Session(self.engine) as session:
|
||||
if isinstance(key, LibraryPrefs):
|
||||
return unwrap(
|
||||
session.scalar(select(Preferences).where(Preferences.key == key.name))
|
||||
).value # pyright: ignore[reportUnknownVariableType]
|
||||
else:
|
||||
return unwrap(
|
||||
session.scalar(select(Preferences).where(Preferences.key == key))
|
||||
).value # pyright: ignore[reportUnknownVariableType]
|
||||
|
||||
# TODO: Remove this once the 'preferences' table is removed.
|
||||
@deprecated("Use `get_version() for version and `ts_ignore` system for extension exclusion.")
|
||||
def set_prefs(self, key: str | LibraryPrefs, value: Any) -> None: # pyright: ignore[reportExplicitAny]
|
||||
# set given item in Preferences table
|
||||
with Session(self.engine) as session:
|
||||
# load existing preference and update value
|
||||
stuff = session.scalars(select(Preferences))
|
||||
logger.info([x.key for x in list(stuff)])
|
||||
|
||||
pref: Preferences = unwrap(
|
||||
session.scalar(
|
||||
select(Preferences).where(
|
||||
Preferences.key == (key.name if isinstance(key, LibraryPrefs) else key)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
logger.info("loading pref", pref=pref, key=key, value=value)
|
||||
pref.value = value
|
||||
session.add(pref)
|
||||
session.commit()
|
||||
# TODO - try/except
|
||||
|
||||
def mirror_entry_fields(self, *entries: Entry) -> None:
|
||||
"""Mirror fields among multiple Entry items."""
|
||||
fields = {}
|
||||
|
||||
@@ -6,9 +6,8 @@ from datetime import datetime as dt
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
from sqlalchemy import JSON, ForeignKey, ForeignKeyConstraint, Integer, event
|
||||
from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, event
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE
|
||||
from tagstudio.core.library.alchemy.db import Base, PathType
|
||||
@@ -327,16 +326,6 @@ def slugify_field_key(mapper, connection, target): # pyright: ignore
|
||||
target.key = slugify(target.tag)
|
||||
|
||||
|
||||
# NOTE: The "Preferences" table has been depreciated as of TagStudio 9.5.4
|
||||
# and is set to be removed in a future release.
|
||||
@deprecated("Use `Version` for storing version, and `ts_ignore` system for file exclusion.")
|
||||
class Preferences(Base):
|
||||
__tablename__ = "preferences"
|
||||
|
||||
key: Mapped[str] = mapped_column(primary_key=True)
|
||||
value: Mapped[dict] = mapped_column(JSON, nullable=False)
|
||||
|
||||
|
||||
class Version(Base):
|
||||
__tablename__ = "versions"
|
||||
|
||||
|
||||
@@ -91,6 +91,23 @@ def ignore_to_glob(ignore_patterns: list[str]) -> list[str]:
|
||||
return glob_patterns
|
||||
|
||||
|
||||
def migrate_ext_list(exts: list[str], is_exclude_list: bool) -> str:
|
||||
# read template
|
||||
ts_ignore_template = (
|
||||
Path(__file__).parents[2] / "resources/templates/ts_ignore_template_blank.txt"
|
||||
)
|
||||
with open(ts_ignore_template) as f:
|
||||
out = f.read()
|
||||
|
||||
# actual conversion
|
||||
prefix = ""
|
||||
if not is_exclude_list:
|
||||
prefix = "!"
|
||||
out += "*\n"
|
||||
out += "\n".join([f"{prefix}*.{x.lstrip('.')}\n" for x in exts])
|
||||
return out
|
||||
|
||||
|
||||
class Ignore(metaclass=Singleton):
|
||||
"""Class for processing and managing glob-like file ignore file patterns."""
|
||||
|
||||
|
||||
@@ -193,18 +193,31 @@ class TagStudioCore:
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=1)
|
||||
def get_most_recent_release_version() -> str:
|
||||
"""Get the version of the most recent Github release."""
|
||||
resp = requests.get("https://api.github.com/repos/TagStudioDev/TagStudio/releases/latest")
|
||||
assert resp.status_code == 200, "Could not fetch information on latest release."
|
||||
def get_most_recent_release_version() -> str | None:
|
||||
"""Get the version of the most recent GitHub release."""
|
||||
try:
|
||||
resp = requests.get(
|
||||
"https://api.github.com/repos/TagStudioDev/TagStudio/releases/latest"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error getting most recent GitHub release.", error=e)
|
||||
return None
|
||||
|
||||
if resp.status_code != 200:
|
||||
logger.error("Error getting most recent GitHub release.", status_code=resp.status_code)
|
||||
return None
|
||||
|
||||
data = resp.json()
|
||||
tag: str = data["tag_name"]
|
||||
assert tag.startswith("v")
|
||||
if not tag.startswith("v"):
|
||||
logger.error("Unexpected tag format.", tag=tag)
|
||||
return None
|
||||
|
||||
version = tag[1:]
|
||||
# the assert does not allow for prerelease/build,
|
||||
# the assertion does not allow for prerelease/build,
|
||||
# because the latest release should never have them
|
||||
assert re.match(r"^\d+\.\d+\.\d+$", version) is not None, "Invalid version format."
|
||||
if re.match(r"^\d+\.\d+\.\d+$", version) is None:
|
||||
logger.error("Invalid version format.", version=version)
|
||||
return None
|
||||
|
||||
return version
|
||||
|
||||
@@ -4,6 +4,7 @@ from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from tagstudio.core.constants import VERSION
|
||||
from tagstudio.core.ts_core import TagStudioCore
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
@@ -30,7 +31,7 @@ class OutOfDateMessageBox(QMessageBox):
|
||||
|
||||
red = get_ui_color(ColorType.PRIMARY, UiColor.RED)
|
||||
green = get_ui_color(ColorType.PRIMARY, UiColor.GREEN)
|
||||
latest_release_version = TagStudioCore.get_most_recent_release_version()
|
||||
latest_release_version = unwrap(TagStudioCore.get_most_recent_release_version())
|
||||
status = Translations.format(
|
||||
"version_modal.status",
|
||||
installed_version=f"<span style='color:{red}'>{VERSION}</span>",
|
||||
|
||||
@@ -49,8 +49,8 @@ class PreviewThumb(PreviewThumbView):
|
||||
stats.width = image.width
|
||||
stats.height = image.height
|
||||
except (
|
||||
rawpy._rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue]
|
||||
rawpy._rawpy.LibRawFileUnsupportedError, # pyright: ignore[reportAttributeAccessIssue]
|
||||
rawpy.LibRawIOError,
|
||||
rawpy.LibRawFileUnsupportedError,
|
||||
FileNotFoundError,
|
||||
):
|
||||
pass
|
||||
|
||||
@@ -21,6 +21,7 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.core.constants import VERSION, VERSION_BRANCH
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.ts_core import TagStudioCore
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.previews.vendored import ffmpeg
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
@@ -106,7 +107,7 @@ class AboutModal(QWidget):
|
||||
|
||||
# Version
|
||||
version_title = QLabel("Version")
|
||||
most_recent_release = TagStudioCore.get_most_recent_release_version()
|
||||
most_recent_release = unwrap(TagStudioCore.get_most_recent_release_version(), "UNKNOWN")
|
||||
version_content_style = self.form_content_style
|
||||
if most_recent_release == VERSION:
|
||||
version_content = QLabel(f"{VERSION}")
|
||||
|
||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import structlog
|
||||
import wcmatch.fnmatch as fnmatch
|
||||
from PySide6.QtCore import QObject, Qt, QThreadPool, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -24,18 +25,19 @@ from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from tagstudio.core.constants import (
|
||||
IGNORE_NAME,
|
||||
LEGACY_TAG_FIELD_IDS,
|
||||
TAG_ARCHIVED,
|
||||
TAG_FAVORITE,
|
||||
TAG_META,
|
||||
TS_FOLDER_NAME,
|
||||
)
|
||||
from tagstudio.core.enums import LibraryPrefs
|
||||
from tagstudio.core.library.alchemy import default_color_groups
|
||||
from tagstudio.core.library.alchemy.constants import SQL_FILENAME
|
||||
from tagstudio.core.library.alchemy.joins import TagParent
|
||||
from tagstudio.core.library.alchemy.library import Library as SqliteLibrary
|
||||
from tagstudio.core.library.alchemy.models import Entry, TagAlias
|
||||
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob
|
||||
from tagstudio.core.library.json.library import Library as JsonLibrary
|
||||
from tagstudio.core.library.json.library import Tag as JsonTag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
@@ -72,8 +74,6 @@ class JsonMigrationModal(QObject):
|
||||
|
||||
self.old_entry_count: int = 0
|
||||
self.old_tag_count: int = 0
|
||||
self.old_ext_count: int = 0
|
||||
self.old_ext_type: bool = None # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
self.field_parity: bool = False
|
||||
self.path_parity: bool = False
|
||||
@@ -82,6 +82,7 @@ class JsonMigrationModal(QObject):
|
||||
self.subtag_parity: bool = False
|
||||
self.alias_parity: bool = False
|
||||
self.color_parity: bool = False
|
||||
self.ext_parity: bool = False
|
||||
|
||||
self.init_page_info()
|
||||
self.init_page_convert()
|
||||
@@ -129,8 +130,7 @@ class JsonMigrationModal(QObject):
|
||||
parent_tags_text: str = tab + Translations["json_migration.heading.parent_tags"]
|
||||
aliases_text: str = tab + Translations["json_migration.heading.aliases"]
|
||||
colors_text: str = tab + Translations["json_migration.heading.colors"]
|
||||
ext_text: str = Translations["json_migration.heading.file_extension_list"]
|
||||
ext_type_text: str = Translations["json_migration.heading.extension_list_type"]
|
||||
ext_parity_text: str = Translations["json_migration.heading.extensions"]
|
||||
desc_text: str = Translations["json_migration.description"]
|
||||
path_parity_text: str = tab + Translations["json_migration.heading.paths"]
|
||||
field_parity_text: str = tab + Translations["library_info.stats.fields"]
|
||||
@@ -145,7 +145,6 @@ class JsonMigrationModal(QObject):
|
||||
self.aliases_row: int = 7
|
||||
self.colors_row: int = 8
|
||||
self.ext_row: int = 9
|
||||
self.ext_type_row: int = 10
|
||||
|
||||
old_lib_container: QWidget = QWidget()
|
||||
old_lib_layout: QVBoxLayout = QVBoxLayout(old_lib_container)
|
||||
@@ -166,8 +165,7 @@ class JsonMigrationModal(QObject):
|
||||
self.old_content_layout.addWidget(QLabel(parent_tags_text), self.parent_tags_row, 0)
|
||||
self.old_content_layout.addWidget(QLabel(aliases_text), self.aliases_row, 0)
|
||||
self.old_content_layout.addWidget(QLabel(colors_text), self.colors_row, 0)
|
||||
self.old_content_layout.addWidget(QLabel(ext_text), self.ext_row, 0)
|
||||
self.old_content_layout.addWidget(QLabel(ext_type_text), self.ext_type_row, 0)
|
||||
self.old_content_layout.addWidget(QLabel(ext_parity_text), self.ext_row, 0)
|
||||
|
||||
old_entry_count: QLabel = QLabel()
|
||||
old_entry_count.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
@@ -187,10 +185,8 @@ class JsonMigrationModal(QObject):
|
||||
old_alias_value.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
old_color_value: QLabel = QLabel()
|
||||
old_color_value.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
old_ext_count: QLabel = QLabel()
|
||||
old_ext_count.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
old_ext_type: QLabel = QLabel()
|
||||
old_ext_type.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
old_ext_value: QLabel = QLabel()
|
||||
old_ext_value.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
self.old_content_layout.addWidget(old_entry_count, self.entries_row, 1)
|
||||
self.old_content_layout.addWidget(old_path_value, self.path_row, 1)
|
||||
@@ -201,8 +197,7 @@ class JsonMigrationModal(QObject):
|
||||
self.old_content_layout.addWidget(old_subtag_value, self.parent_tags_row, 1)
|
||||
self.old_content_layout.addWidget(old_alias_value, self.aliases_row, 1)
|
||||
self.old_content_layout.addWidget(old_color_value, self.colors_row, 1)
|
||||
self.old_content_layout.addWidget(old_ext_count, self.ext_row, 1)
|
||||
self.old_content_layout.addWidget(old_ext_type, self.ext_type_row, 1)
|
||||
self.old_content_layout.addWidget(old_ext_value, self.ext_row, 1)
|
||||
|
||||
self.old_content_layout.addWidget(QLabel(), self.path_row, 2)
|
||||
self.old_content_layout.addWidget(QLabel(), self.fields_row, 2)
|
||||
@@ -211,6 +206,7 @@ class JsonMigrationModal(QObject):
|
||||
self.old_content_layout.addWidget(QLabel(), self.parent_tags_row, 2)
|
||||
self.old_content_layout.addWidget(QLabel(), self.aliases_row, 2)
|
||||
self.old_content_layout.addWidget(QLabel(), self.colors_row, 2)
|
||||
self.old_content_layout.addWidget(QLabel(), self.ext_row, 2)
|
||||
|
||||
old_lib_layout.addWidget(old_content_container)
|
||||
|
||||
@@ -233,8 +229,7 @@ class JsonMigrationModal(QObject):
|
||||
self.new_content_layout.addWidget(QLabel(parent_tags_text), self.parent_tags_row, 0)
|
||||
self.new_content_layout.addWidget(QLabel(aliases_text), self.aliases_row, 0)
|
||||
self.new_content_layout.addWidget(QLabel(colors_text), self.colors_row, 0)
|
||||
self.new_content_layout.addWidget(QLabel(ext_text), self.ext_row, 0)
|
||||
self.new_content_layout.addWidget(QLabel(ext_type_text), self.ext_type_row, 0)
|
||||
self.new_content_layout.addWidget(QLabel(ext_parity_text), self.ext_row, 0)
|
||||
|
||||
new_entry_count: QLabel = QLabel()
|
||||
new_entry_count.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
@@ -254,10 +249,8 @@ class JsonMigrationModal(QObject):
|
||||
alias_parity_value.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
new_color_value: QLabel = QLabel()
|
||||
new_color_value.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
new_ext_count: QLabel = QLabel()
|
||||
new_ext_count.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
new_ext_type: QLabel = QLabel()
|
||||
new_ext_type.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
ext_parity_value: QLabel = QLabel()
|
||||
ext_parity_value.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
self.new_content_layout.addWidget(new_entry_count, self.entries_row, 1)
|
||||
self.new_content_layout.addWidget(path_parity_value, self.path_row, 1)
|
||||
@@ -268,8 +261,7 @@ class JsonMigrationModal(QObject):
|
||||
self.new_content_layout.addWidget(subtag_parity_value, self.parent_tags_row, 1)
|
||||
self.new_content_layout.addWidget(alias_parity_value, self.aliases_row, 1)
|
||||
self.new_content_layout.addWidget(new_color_value, self.colors_row, 1)
|
||||
self.new_content_layout.addWidget(new_ext_count, self.ext_row, 1)
|
||||
self.new_content_layout.addWidget(new_ext_type, self.ext_type_row, 1)
|
||||
self.new_content_layout.addWidget(ext_parity_value, self.ext_row, 1)
|
||||
|
||||
self.new_content_layout.addWidget(QLabel(), self.entries_row, 2)
|
||||
self.new_content_layout.addWidget(QLabel(), self.path_row, 2)
|
||||
@@ -281,7 +273,6 @@ class JsonMigrationModal(QObject):
|
||||
self.new_content_layout.addWidget(QLabel(), self.aliases_row, 2)
|
||||
self.new_content_layout.addWidget(QLabel(), self.colors_row, 2)
|
||||
self.new_content_layout.addWidget(QLabel(), self.ext_row, 2)
|
||||
self.new_content_layout.addWidget(QLabel(), self.ext_type_row, 2)
|
||||
|
||||
new_lib_layout.addWidget(new_content_container)
|
||||
|
||||
@@ -334,8 +325,6 @@ class JsonMigrationModal(QObject):
|
||||
# Update JSON UI
|
||||
self.update_json_entry_count(len(self.json_lib.entries))
|
||||
self.update_json_tag_count(len(self.json_lib.tags))
|
||||
self.update_json_ext_count(len(self.json_lib.ext_list))
|
||||
self.update_json_ext_type(self.json_lib.is_exclude_list)
|
||||
|
||||
self.migration_progress(skip_ui=skip_ui)
|
||||
self.is_migration_initialized = True
|
||||
@@ -410,11 +399,12 @@ class JsonMigrationModal(QObject):
|
||||
self.temp_path: Path = (
|
||||
self.json_lib.library_dir / TS_FOLDER_NAME / "migration_ts_library.sqlite"
|
||||
)
|
||||
self.sql_lib.storage_path = self.temp_path
|
||||
if self.temp_path.exists():
|
||||
logger.info('Temporary migration file "temp_path" already exists. Removing...')
|
||||
self.temp_path.unlink()
|
||||
self.sql_lib.open_sqlite_library(self.json_lib.library_dir, is_new=True)
|
||||
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)
|
||||
)
|
||||
@@ -428,6 +418,7 @@ class JsonMigrationModal(QObject):
|
||||
check_set.add(self.check_subtag_parity())
|
||||
check_set.add(self.check_alias_parity())
|
||||
check_set.add(self.check_color_parity())
|
||||
check_set.add(self.check_ignore_parity())
|
||||
if False not in check_set:
|
||||
yield Translations["json_migration.migration_complete"]
|
||||
else:
|
||||
@@ -454,6 +445,7 @@ class JsonMigrationModal(QObject):
|
||||
self.update_parity_value(self.parent_tags_row, self.subtag_parity)
|
||||
self.update_parity_value(self.aliases_row, self.alias_parity)
|
||||
self.update_parity_value(self.colors_row, self.color_parity)
|
||||
self.update_parity_value(self.ext_row, self.ext_parity)
|
||||
self.sql_lib.close()
|
||||
|
||||
def update_sql_value_ui(self, show_msg_box: bool = True):
|
||||
@@ -468,16 +460,6 @@ class JsonMigrationModal(QObject):
|
||||
len(self.sql_lib.tags),
|
||||
self.old_tag_count,
|
||||
)
|
||||
self.update_sql_value(
|
||||
self.ext_row,
|
||||
len(self.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST)),
|
||||
self.old_ext_count,
|
||||
)
|
||||
self.update_sql_value(
|
||||
self.ext_type_row,
|
||||
self.sql_lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST), # pyright: ignore[reportArgumentType]
|
||||
self.old_ext_type,
|
||||
)
|
||||
logger.info("Parity check complete!")
|
||||
if self.discrepancies:
|
||||
logger.warning("Discrepancies found:")
|
||||
@@ -509,16 +491,6 @@ class JsonMigrationModal(QObject):
|
||||
label: QLabel = self.old_content_layout.itemAtPosition(self.tags_row, 1).widget() # type:ignore
|
||||
label.setText(self.color_value_default(value))
|
||||
|
||||
def update_json_ext_count(self, value: int):
|
||||
self.old_ext_count = value
|
||||
label: QLabel = self.old_content_layout.itemAtPosition(self.ext_row, 1).widget() # type:ignore
|
||||
label.setText(self.color_value_default(value))
|
||||
|
||||
def update_json_ext_type(self, value: bool):
|
||||
self.old_ext_type = value
|
||||
label: QLabel = self.old_content_layout.itemAtPosition(self.ext_type_row, 1).widget() # type:ignore
|
||||
label.setText(self.color_value_default(value))
|
||||
|
||||
def update_sql_value(self, row: int, value: int | bool, old_value: int | bool):
|
||||
label: QLabel = self.new_content_layout.itemAtPosition(row, 1).widget() # type:ignore
|
||||
warning_icon: QLabel = self.new_content_layout.itemAtPosition(row, 2).widget() # type:ignore
|
||||
@@ -547,6 +519,28 @@ class JsonMigrationModal(QObject):
|
||||
color = green if old_value == new_value else red
|
||||
return str(f"<b><a style='color: {color}'>{new_value}</a></b>")
|
||||
|
||||
def assert_ignore_parity(self) -> None:
|
||||
compiled_pats = fnmatch.compile(
|
||||
ignore_to_glob(
|
||||
Ignore._load_ignore_file(
|
||||
unwrap(self.json_lib.library_dir) / TS_FOLDER_NAME / IGNORE_NAME
|
||||
)
|
||||
),
|
||||
PATH_GLOB_FLAGS,
|
||||
) # copied from Ignore.get_patterns since that method modifies singleton state
|
||||
path = self.json_lib.library_dir / "filename"
|
||||
for ext in self.json_lib.ext_list:
|
||||
assert compiled_pats.match(str(path / ext)) == self.json_lib.is_exclude_list
|
||||
assert compiled_pats.match(str(path / ".not_a_real_ext")) != self.json_lib.is_exclude_list
|
||||
|
||||
def check_ignore_parity(self) -> bool:
|
||||
try:
|
||||
self.assert_ignore_parity()
|
||||
self.ext_parity = True
|
||||
except AssertionError:
|
||||
self.ext_parity = False
|
||||
return self.ext_parity
|
||||
|
||||
def check_field_parity(self) -> bool:
|
||||
"""Check if all JSON field and tag data matches the new SQL data."""
|
||||
|
||||
@@ -670,9 +664,6 @@ class JsonMigrationModal(QObject):
|
||||
self.subtag_parity = True
|
||||
return self.subtag_parity
|
||||
|
||||
def check_ext_type(self) -> bool:
|
||||
return self.json_lib.is_exclude_list == self.sql_lib.prefs(LibraryPrefs.IS_EXCLUDE_LIST)
|
||||
|
||||
def check_alias_parity(self) -> bool:
|
||||
"""Check if all JSON aliases match the new SQL aliases."""
|
||||
with Session(self.sql_lib.engine) as session:
|
||||
|
||||
@@ -1121,8 +1121,8 @@ class ThumbRenderer(QObject):
|
||||
)
|
||||
except (
|
||||
DecompressionBombError,
|
||||
rawpy._rawpy.LibRawIOError, # pyright: ignore[reportAttributeAccessIssue]
|
||||
rawpy._rawpy.LibRawFileUnsupportedError, # pyright: ignore[reportAttributeAccessIssue]
|
||||
rawpy.LibRawIOError,
|
||||
rawpy.LibRawFileUnsupportedError,
|
||||
) as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
@@ -613,7 +613,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
if not which(FFMPEG_CMD) or not which(FFPROBE_CMD):
|
||||
FfmpegMissingMessageBox().show()
|
||||
|
||||
if is_version_outdated(VERSION, TagStudioCore.get_most_recent_release_version()):
|
||||
latest_version = TagStudioCore.get_most_recent_release_version()
|
||||
if latest_version and is_version_outdated(VERSION, latest_version):
|
||||
OutOfDateMessageBox().exec()
|
||||
|
||||
self.app.exec()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -157,8 +157,6 @@
|
||||
"json_migration.heading.aliases": "Aliase:",
|
||||
"json_migration.heading.colors": "Farben:",
|
||||
"json_migration.heading.differ": "Diskrepanz",
|
||||
"json_migration.heading.extension_list_type": "Erweiterungslistentyp:",
|
||||
"json_migration.heading.file_extension_list": "Liste der Dateiendungen:",
|
||||
"json_migration.heading.match": "Übereinstimmend",
|
||||
"json_migration.heading.names": "Namen:",
|
||||
"json_migration.heading.parent_tags": "Übergeordnete Tags:",
|
||||
|
||||
@@ -157,8 +157,6 @@
|
||||
"json_migration.heading.aliases": "Ψευδώνυμα:",
|
||||
"json_migration.heading.colors": "Χρώματα:",
|
||||
"json_migration.heading.differ": "Ασυμφωνία",
|
||||
"json_migration.heading.extension_list_type": "Τύπος λίστας επεκτάσεων:",
|
||||
"json_migration.heading.file_extension_list": "Λίστα επεκτάσεων αρχείων:",
|
||||
"json_migration.heading.match": "Αντιστοιχίστηκε",
|
||||
"json_migration.heading.names": "Όνομα:"
|
||||
}
|
||||
|
||||
@@ -157,8 +157,7 @@
|
||||
"json_migration.heading.aliases": "Aliases:",
|
||||
"json_migration.heading.colors": "Colors:",
|
||||
"json_migration.heading.differ": "Discrepancy",
|
||||
"json_migration.heading.extension_list_type": "Extension List Type:",
|
||||
"json_migration.heading.file_extension_list": "File Extension List:",
|
||||
"json_migration.heading.extensions": "Extensions:",
|
||||
"json_migration.heading.match": "Matched",
|
||||
"json_migration.heading.names": "Names:",
|
||||
"json_migration.heading.parent_tags": "Parent Tags:",
|
||||
|
||||
@@ -157,8 +157,6 @@
|
||||
"json_migration.heading.aliases": "Alias:",
|
||||
"json_migration.heading.colors": "Colores:",
|
||||
"json_migration.heading.differ": "Discrepancia",
|
||||
"json_migration.heading.extension_list_type": "Tipo de lista de extensión:",
|
||||
"json_migration.heading.file_extension_list": "Lista de extensiones de archivos:",
|
||||
"json_migration.heading.match": "Igualado",
|
||||
"json_migration.heading.names": "Nombres:",
|
||||
"json_migration.heading.parent_tags": "Etiquetas principales:",
|
||||
|
||||
@@ -157,8 +157,6 @@
|
||||
"json_migration.heading.aliases": "Aliaksia:",
|
||||
"json_migration.heading.colors": "Värit:",
|
||||
"json_migration.heading.differ": "Poikkeavuus",
|
||||
"json_migration.heading.extension_list_type": "Laajennusluettelon tyyppi:",
|
||||
"json_migration.heading.file_extension_list": "Tiedostopääte lista:",
|
||||
"json_migration.heading.match": "Yhdistetty",
|
||||
"json_migration.heading.names": "Nimet:",
|
||||
"json_migration.heading.parent_tags": "Päätunnukset:",
|
||||
|
||||
@@ -140,8 +140,6 @@
|
||||
"json_migration.heading.aliases": "Mga alyas:",
|
||||
"json_migration.heading.colors": "Mga kulay:",
|
||||
"json_migration.heading.differ": "May pagkakaiba",
|
||||
"json_migration.heading.extension_list_type": "Uri ng Listahan ng Extension:",
|
||||
"json_migration.heading.file_extension_list": "Listahan ng Mga File Extension:",
|
||||
"json_migration.heading.match": "Tumutugma",
|
||||
"json_migration.heading.names": "Mga pangalan:",
|
||||
"json_migration.heading.parent_tags": "Mga parent tag:",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"about.config_path": "Chemin de Configuration",
|
||||
"about.description": "TagStudio est une application d'organisation de photos et de fichiers avec un système de tags qui mets en avant la liberté et flexibilité à l'utilisateur. Pas de programmes ou de formats propriétaires, pas la moindre trace de fichiers secondaires, et pas de bouleversement complet de la structure de votre système de fichiers.",
|
||||
"about.description": "TagStudio est une application d'organisation de photos et de fichiers avec un système de tags qui met en avant la liberté et flexibilité à l'utilisateur. Pas de programmes ou de formats propriétaires, pas la moindre trace de fichiers secondaires, et pas de bouleversement complet de la structure de votre système de fichiers.",
|
||||
"about.documentation": "Documentation",
|
||||
"about.license": "Licence",
|
||||
"about.module.found": "Trouver",
|
||||
"about.module.found": "Trouvé",
|
||||
"about.title": "À propos de TagStudio",
|
||||
"about.website": "Site Internet",
|
||||
"app.git": "Git Commit",
|
||||
@@ -12,10 +12,10 @@
|
||||
"color.color_border": "Utiliser la couleur secondaire sur la bordure",
|
||||
"color.confirm_delete": "Voulez vous vraiment supprimer la couleur \"{color_name}\"?",
|
||||
"color.delete": "Supprimer le Tag",
|
||||
"color.import_pack": "Importer un Pack de Couleur",
|
||||
"color.import_pack": "Importer un Pack de Couleurs",
|
||||
"color.name": "Nom",
|
||||
"color.namespace.delete.prompt": "Voulez-vous vraiment supprimer ce groupe de couleurs? Cela supprimera TOUTES les couleurs du groupe!",
|
||||
"color.namespace.delete.title": "Supprimer le namespace de couleur",
|
||||
"color.namespace.delete.prompt": "Voulez-vous vraiment supprimer ce groupe de couleurs ? Cela supprimera TOUTES les couleurs du groupe !",
|
||||
"color.namespace.delete.title": "Supprimer l'espace de noms de couleurs",
|
||||
"color.new": "Nouvelle couleur",
|
||||
"color.placeholder": "Couleur",
|
||||
"color.primary": "Couleur Primaire",
|
||||
@@ -33,8 +33,8 @@
|
||||
"drop_import.progress.window_title": "Importer des Fichiers",
|
||||
"drop_import.title": "Fichier(s) en Conflit",
|
||||
"edit.color_manager": "Gérer la Couleur des Tags",
|
||||
"edit.copy_fields": "Copier les Fields",
|
||||
"edit.paste_fields": "Coller les Fields",
|
||||
"edit.copy_fields": "Copier les Champs",
|
||||
"edit.paste_fields": "Coller les Champs",
|
||||
"edit.tag_manager": "Gérer les Tags",
|
||||
"entries.duplicate.merge": "Fusion des entrées dupliquées",
|
||||
"entries.duplicate.merge.label": "Fusionner les entrées dupliquées...",
|
||||
@@ -42,19 +42,19 @@
|
||||
"entries.duplicates.description": "Les entrées dupliquées sont définies comme des entrées multiple qui pointent vers le même fichier sur le disque. Les fusionner va combiner les tags et metadatas de tous les duplicatas vers une seule entrée consolidée. Elles ne doivent pas être confondues avec les \"fichiers en doublon\", qui sont des doublons de vos fichiers en dehors de TagStudio.",
|
||||
"entries.generic.refresh_alt": "&Recharger",
|
||||
"entries.generic.remove.removing": "Suppression des Entrées",
|
||||
"entries.generic.remove.removing_count": "Suppressions de {count} entrées...",
|
||||
"entries.generic.remove.removing_count": "Suppression de {count} entrées...",
|
||||
"entries.ignored.description": "Les entrées de fichier sont considérées comme « ignorées » si elles ont été ajoutées à la bibliothèque avant que les règles d'ignorance de l'utilisateur (via le fichier « .ts_ignore ») aient été mises à jour pour les exclure. Les fichiers ignorés sont conservés dans la bibliothèque par défaut afin d'éviter toute perte accidentelle de données lors de la mise à jour des règles d'ignorance.",
|
||||
"entries.ignored.ignored_count": "Entrées Ignorées : {count}",
|
||||
"entries.ignored.remove": "Supprimer les entrées ignorées",
|
||||
"entries.ignored.remove_alt": "Supprim&er les entrées ignorées",
|
||||
"entries.ignored.scanning": "Recherche des entrées ignorées dans la bibliothèque...",
|
||||
"entries.ignored.title": "Corriger les entrées ignorées",
|
||||
"entries.mirror": "&Refléter",
|
||||
"entries.mirror.confirmation": "Êtes-vous sûr de vouloir répliquer les {count} Entrées suivantes ?",
|
||||
"entries.mirror": "&Répliquer",
|
||||
"entries.mirror.confirmation": "Êtes-vous sûr de vouloir répliquer les {count} Entrées suivantes ?",
|
||||
"entries.mirror.label": "Réplication de {idx}/{total} Entrées...",
|
||||
"entries.mirror.title": "Réplication des Entrées",
|
||||
"entries.mirror.window_title": "Entrée Miroir",
|
||||
"entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les <b>{count}</b> entrées suivantes ? Aucun fichiers sur votre disque ne sera supprimée.",
|
||||
"entries.mirror.window_title": "Entrées Répliqués",
|
||||
"entries.remove.plural.confirm": "Êtes-vous sûr de vouloir supprimer les <b>{count}</b> entrées suivantes ? Aucun fichier sur votre disque ne sera supprimée.",
|
||||
"entries.remove.singular.confirm": "Êtes-vous sûr de vouloir supprimer cette entrée de votre bibliothèque ? Aucun fichier sur le disque ne sera supprimé.",
|
||||
"entries.running.dialog.new_entries": "Ajout de {total} Nouvelles entrées de fichier...",
|
||||
"entries.running.dialog.title": "Ajout de Nouvelles entrées de fichier",
|
||||
@@ -68,7 +68,7 @@
|
||||
"entries.unlinked.scanning": "Balayage de la Bibliothèque pour trouver des Entrées non Liées...",
|
||||
"entries.unlinked.search_and_relink": "&Rechercher && Relier",
|
||||
"entries.unlinked.title": "Réparation des Entrées non Liées",
|
||||
"entries.unlinked.unlinked_count": "Entrées non Liées : {count}",
|
||||
"entries.unlinked.unlinked_count": "Entrées non Liées : {count}",
|
||||
"ffmpeg.missing.description": "FFmpeg et/ou FFprobe n’ont pas été trouvée. FFmpeg est nécessaire pour la lecture de média et les vignettes.",
|
||||
"ffmpeg.missing.status": "{ffmpeg} : {ffmpeg_status}<br>{ffprobe} : {ffprobe_status}",
|
||||
"field.copy": "Copier le Champ",
|
||||
@@ -83,25 +83,25 @@
|
||||
"file.duplicates.dupeguru.file_extension": "Fichiers DupeGuru (*.dupeguru)",
|
||||
"file.duplicates.dupeguru.load_file": "&Charger un Fichier DupeGuru",
|
||||
"file.duplicates.dupeguru.no_file": "Aucun Fichier DupeGuru Sélectionné",
|
||||
"file.duplicates.dupeguru.open_file": "Ouvrire les Fichiers de Résultats de DupeGuru",
|
||||
"file.duplicates.dupeguru.open_file": "Ouvrir les Fichiers de Résultats de DupeGuru",
|
||||
"file.duplicates.fix": "Réparer les Fichiers en Double",
|
||||
"file.duplicates.matches": "Dupliquer les Correspondances de Fichier: {count}",
|
||||
"file.duplicates.matches_uninitialized": "Dupliquer les Correspondances de Fichier : N/A",
|
||||
"file.duplicates.matches_uninitialized": "Dupliquer les Correspondances de Fichier : N/A",
|
||||
"file.duplicates.mirror.description": "Repliquer les données d'entrée dans chaque jeu de correspondances en double, en combinant toutes les données sans supprimer ni dupliquer de champs. Cette opération ne supprime aucun fichier ni aucune donnée.",
|
||||
"file.duplicates.mirror_entries": "&Répliquer les Entrées",
|
||||
"file.duration": "Durée",
|
||||
"file.not_found": "Fichier non trouvé",
|
||||
"file.open_file": "Ouvrir un Fichier",
|
||||
"file.open_file_with": "Ouvrir le fichier avec",
|
||||
"file.open_location.generic": "Ouvrir le Fichier dans l'Explorateur de Fichier",
|
||||
"file.open_location.generic": "Montrer le fichier dans l'Explorateur de Fichier",
|
||||
"file.open_location.mac": "Montrer dans le Finder",
|
||||
"file.open_location.windows": "Montrer dans l'explorateur de Fichiers",
|
||||
"file.path": "Chemin du Fichier",
|
||||
"folders_to_tags.close_all": "Tout Fermer",
|
||||
"folders_to_tags.converting": "Conversion des dossiers en Tags",
|
||||
"folders_to_tags.description": "Créé des Tags basés sur votre arborescence de dossier et les applique à vos entrées.\nLa structure ci-dessous affiche tous les labels qui seront créés et à quelles entrées ils seront appliqués.",
|
||||
"folders_to_tags.description": "Créé des Tags basés sur votre arborescence de dossier et les applique à vos entrées.\nLa structure ci-dessous affiche tous les Tags qui seront créés et à quelles entrées ils seront appliqués.",
|
||||
"folders_to_tags.open_all": "Tout Ouvrir",
|
||||
"folders_to_tags.title": "Créer un Label à partir d'un Dossier",
|
||||
"folders_to_tags.title": "Créer un Tag à partir d'un Dossier",
|
||||
"generic.add": "Ajouter",
|
||||
"generic.apply": "Appliquer",
|
||||
"generic.apply_alt": "&Appliquer",
|
||||
@@ -115,7 +115,7 @@
|
||||
"generic.delete_alt": "&Supprimer",
|
||||
"generic.done": "Terminé",
|
||||
"generic.done_alt": "&Terminé",
|
||||
"generic.edit": "Éditer",
|
||||
"generic.edit": "Modifier",
|
||||
"generic.edit_alt": "&Modifier",
|
||||
"generic.filename": "Nom de fichier",
|
||||
"generic.missing": "Manquant",
|
||||
@@ -150,15 +150,13 @@
|
||||
"ignore.open_file": "Afficher le fichier \"{ts_ignore}\" sur le Disque",
|
||||
"json_migration.checking_for_parity": "Vérification de la Parité...",
|
||||
"json_migration.creating_database_tables": "Création des Tables de Base de Données SQL...",
|
||||
"json_migration.description": "<br>Démarrez et prévisualisez les résultats du processus de migration de la bibliothèque. La bibliothèque convertie <i>ne</i> sera utilisée que si vous cliquez sur \"Terminer la migration\". <br><br>Les données de la bibliothèque doivent soit avoir des valeurs correspondantes, soit comporter un label \"Matched\". Les valeurs qui ne correspondent pas seront affichées en rouge et comporteront un symbole \"<b>(!)</b>\" à côté d'elles.<br><center><i>Ce processus peut prendre jusqu'à plusieurs minutes pour les bibliothèques plus volumineuses.</i></center>",
|
||||
"json_migration.discrepancies_found": "Divergence Détectées dans la Bibliothèque",
|
||||
"json_migration.description": "<br>Démarrez et prévisualisez les résultats du processus de migration de la bibliothèque. La bibliothèque convertie ne sera utilisée <i>que</i> si vous cliquez sur \"Terminer la migration\". <br><br>Les données de la bibliothèque doivent soit avoir des valeurs correspondantes, soit comporter un label \"Matched\". Les valeurs qui ne correspondent pas seront affichées en rouge et comporteront un symbole \"<b>(!)</b>\" à côté d'elles.<br><center><i>Ce processus peut prendre jusqu'à plusieurs minutes pour les bibliothèques plus volumineuses.</i></center>",
|
||||
"json_migration.discrepancies_found": "Divergences Détectées dans la Bibliothèque",
|
||||
"json_migration.discrepancies_found.description": "Des divergences ont été détectées entre le format d'origine et le format converti de la bibliothèque. Veuillez les examiner et choisir de poursuivre la migration ou de l'annuler.",
|
||||
"json_migration.finish_migration": "Terminer la Migration",
|
||||
"json_migration.heading.aliases": "Alias :",
|
||||
"json_migration.heading.colors": "Couleurs :",
|
||||
"json_migration.heading.differ": "Divergence",
|
||||
"json_migration.heading.extension_list_type": "Type de liste d'extension :",
|
||||
"json_migration.heading.file_extension_list": "Liste des extensions de fichiers :",
|
||||
"json_migration.heading.match": "Correspondant",
|
||||
"json_migration.heading.names": "Noms :",
|
||||
"json_migration.heading.parent_tags": "Tags Parents :",
|
||||
|
||||
@@ -157,8 +157,6 @@
|
||||
"json_migration.heading.aliases": "Áljelek:",
|
||||
"json_migration.heading.colors": "Színek:",
|
||||
"json_migration.heading.differ": "Eltérés",
|
||||
"json_migration.heading.extension_list_type": "Kiterjesztési lista típusa:",
|
||||
"json_migration.heading.file_extension_list": "Fájlkiterjesztési lista:",
|
||||
"json_migration.heading.match": "Egységesítve",
|
||||
"json_migration.heading.names": "Nevek:",
|
||||
"json_migration.heading.parent_tags": "Szülőcímkék:",
|
||||
|
||||
@@ -157,8 +157,6 @@
|
||||
"json_migration.heading.aliases": "Alias:",
|
||||
"json_migration.heading.colors": "Colori:",
|
||||
"json_migration.heading.differ": "Discrepanze",
|
||||
"json_migration.heading.extension_list_type": "Tipo di Lista di Entensioni:",
|
||||
"json_migration.heading.file_extension_list": "Elenco Estensioni dei File:",
|
||||
"json_migration.heading.match": "Abbinato",
|
||||
"json_migration.heading.names": "Nomi:",
|
||||
"json_migration.heading.parent_tags": "Etichette Genitore:",
|
||||
|
||||
@@ -157,8 +157,6 @@
|
||||
"json_migration.heading.aliases": "エイリアス:",
|
||||
"json_migration.heading.colors": "色:",
|
||||
"json_migration.heading.differ": "差異",
|
||||
"json_migration.heading.extension_list_type": "拡張子リストの種類:",
|
||||
"json_migration.heading.file_extension_list": "ファイルの拡張子リスト:",
|
||||
"json_migration.heading.match": "一致",
|
||||
"json_migration.heading.names": "名前:",
|
||||
"json_migration.heading.parent_tags": "親タグ:",
|
||||
|
||||
@@ -148,8 +148,6 @@
|
||||
"json_migration.heading.aliases": "Alternative navn:",
|
||||
"json_migration.heading.colors": "Farger:",
|
||||
"json_migration.heading.differ": "Avvik",
|
||||
"json_migration.heading.extension_list_type": "Type av Utvidelsesliste:",
|
||||
"json_migration.heading.file_extension_list": "Filutvidelse Liste:",
|
||||
"json_migration.heading.match": "Matchet",
|
||||
"json_migration.heading.names": "Navn:",
|
||||
"json_migration.heading.parent_tags": "Overordnede Etiketter:",
|
||||
|
||||
@@ -139,8 +139,6 @@
|
||||
"json_migration.heading.aliases": "Zastępcze nazwy:",
|
||||
"json_migration.heading.colors": "Kolory:",
|
||||
"json_migration.heading.differ": "Niezgodność",
|
||||
"json_migration.heading.extension_list_type": "Typ listy rozszerzeń:",
|
||||
"json_migration.heading.file_extension_list": "Lista rozszerzeń plików:",
|
||||
"json_migration.heading.match": "Dopasowane",
|
||||
"json_migration.heading.names": "Nazwy:",
|
||||
"json_migration.heading.parent_tags": "Tagi nadrzędne:",
|
||||
|
||||
@@ -136,8 +136,6 @@
|
||||
"json_migration.heading.aliases": "Pseudônimos:",
|
||||
"json_migration.heading.colors": "Cores:",
|
||||
"json_migration.heading.differ": "Discrepância",
|
||||
"json_migration.heading.extension_list_type": "Tipo de Lista de Extensão:",
|
||||
"json_migration.heading.file_extension_list": "Lista de Extensão de Ficheiro:",
|
||||
"json_migration.heading.match": "Combinado",
|
||||
"json_migration.heading.names": "Nomes:",
|
||||
"json_migration.heading.parent_tags": "Tags Pai:",
|
||||
|
||||
@@ -157,8 +157,6 @@
|
||||
"json_migration.heading.aliases": "Pseudônimos:",
|
||||
"json_migration.heading.colors": "Cores:",
|
||||
"json_migration.heading.differ": "Discrepância",
|
||||
"json_migration.heading.extension_list_type": "Lista dos tipos de Extensão:",
|
||||
"json_migration.heading.file_extension_list": "Lista de Extensão de Arquivo:",
|
||||
"json_migration.heading.match": "Correspondido",
|
||||
"json_migration.heading.names": "Nomes:",
|
||||
"json_migration.heading.parent_tags": "Tags Pai:",
|
||||
|
||||
@@ -137,8 +137,6 @@
|
||||
"json_migration.heading.aliases": "Andrnamae:",
|
||||
"json_migration.heading.colors": "Varge:",
|
||||
"json_migration.heading.differ": "Tchigauzma",
|
||||
"json_migration.heading.extension_list_type": "Fal fu taksanting tumam:",
|
||||
"json_migration.heading.file_extension_list": "Tumam fu mlafufal:",
|
||||
"json_migration.heading.match": "Finnajena sama",
|
||||
"json_migration.heading.names": "Namae:",
|
||||
"json_migration.heading.parent_tags": "Atama festaretol:",
|
||||
|
||||
@@ -35,19 +35,25 @@
|
||||
"edit.color_manager": "Редактировать цвета тегов",
|
||||
"edit.copy_fields": "Копировать поля",
|
||||
"edit.paste_fields": "Вставить поля",
|
||||
"edit.tag_manager": "Управлять тегами",
|
||||
"edit.tag_manager": "Управление тегами",
|
||||
"entries.duplicate.merge": "Объединить записи-дубликаты",
|
||||
"entries.duplicate.merge.label": "Объединение записей-дубликатов...",
|
||||
"entries.duplicate.refresh": "Обновить записи-дубликаты",
|
||||
"entries.duplicates.description": "Записи-дубликаты — это несколько записей, которые одновременно привязаны к одному файлу. Объединение таких дубликатов соединит все теги и мета данные из этих записей в одну. Записи-дубликаты не стоит путать с несколькими копиями самого файла, которые могут существовать вне TagStudio.",
|
||||
"entries.duplicates.description": "Записи-дубликаты — это несколько записей, которые одновременно привязаны к одному файлу. Объединение таких дубликатов соединит все теги и мета данные из этих записей в одну. Записи-дубликаты не стоит путать с копиями самого файла, которые существуют вне TagStudio.",
|
||||
"entries.generic.refresh_alt": "&Обновить",
|
||||
"entries.generic.remove.removing": "Удаление записей",
|
||||
"entries.generic.remove.removing_count": "Удаление {count} записей...",
|
||||
"entries.ignored.ignored_count": "Проигнорированных записей: {count}",
|
||||
"entries.ignored.remove": "Удалить игнорируемые записи",
|
||||
"entries.ignored.scanning": "Поиск игнорируемых записей...",
|
||||
"entries.ignored.title": "Исправить игнорируемые записи",
|
||||
"entries.mirror": "&Отзеркалить",
|
||||
"entries.mirror.confirmation": "Вы уверены, что хотите отзеркалить следующие {count} записей?",
|
||||
"entries.mirror.label": "Отзеркаливание {idx}/{total} записей...",
|
||||
"entries.mirror.title": "Отзеркаливание записей",
|
||||
"entries.mirror.window_title": "Отзеркалить записи",
|
||||
"entries.remove.plural.confirm": "Вы уверены, что хотите удалить {count} записей?",
|
||||
"entries.remove.plural.confirm": "Вы уверены, что хотите удалить <b>{count}</b> записей? Файлы на диске не будут удалены.",
|
||||
"entries.remove.singular.confirm": "Вы уверены, что хотите удалить эту запись? Файл на диске не будет удалён.",
|
||||
"entries.running.dialog.new_entries": "Добавление {total} новых записей...",
|
||||
"entries.running.dialog.title": "Добавление новых записей",
|
||||
"entries.tags": "Теги",
|
||||
@@ -55,6 +61,7 @@
|
||||
"entries.unlinked.relink.attempting": "Попытка перепривязать {index}/{unlinked_count} записей, {fixed_count} привязано успешно",
|
||||
"entries.unlinked.relink.manual": "&Ручная привязка",
|
||||
"entries.unlinked.relink.title": "Привязка записей",
|
||||
"entries.unlinked.remove": "Удалить откреплённые записи",
|
||||
"entries.unlinked.scanning": "Сканирование библиотеки на наличие откреплённых записей...",
|
||||
"entries.unlinked.search_and_relink": "&Поиск и привязка",
|
||||
"entries.unlinked.title": "Исправить откреплённые записи",
|
||||
@@ -141,8 +148,6 @@
|
||||
"json_migration.heading.aliases": "Псевдонимы:",
|
||||
"json_migration.heading.colors": "Цвета:",
|
||||
"json_migration.heading.differ": "Несоответствие",
|
||||
"json_migration.heading.extension_list_type": "Тип списка расширений:",
|
||||
"json_migration.heading.file_extension_list": "Список расширений файлов:",
|
||||
"json_migration.heading.match": "Совпало",
|
||||
"json_migration.heading.names": "Имена:",
|
||||
"json_migration.heading.parent_tags": "Родительские теги:",
|
||||
|
||||
@@ -157,8 +157,6 @@
|
||||
"json_migration.heading.aliases": "மாற்றுப்பெயர்கள்:",
|
||||
"json_migration.heading.colors": "நிறங்கள்:",
|
||||
"json_migration.heading.differ": "முரண்பாடு",
|
||||
"json_migration.heading.extension_list_type": "நீட்டிப்பு பட்டியல் வகை:",
|
||||
"json_migration.heading.file_extension_list": "கோப்பு நீட்டிப்பு பட்டியல்:",
|
||||
"json_migration.heading.match": "பொருந்தியது",
|
||||
"json_migration.heading.names": "பெயர்கள்:",
|
||||
"json_migration.heading.parent_tags": "பெற்றோர் குறிச்சொற்கள்:",
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
"json_migration.heading.aliases": "nimi ante:",
|
||||
"json_migration.heading.colors": "kule:",
|
||||
"json_migration.heading.differ": "ante ike",
|
||||
"json_migration.heading.file_extension_list": "kulupu pi namako lipu:",
|
||||
"json_migration.heading.match": "sama",
|
||||
"json_migration.heading.names": "nimi:",
|
||||
"json_migration.heading.parent_tags": "poki mama:",
|
||||
|
||||
@@ -136,8 +136,6 @@
|
||||
"json_migration.heading.aliases": "Takma Adlar:",
|
||||
"json_migration.heading.colors": "Renkler:",
|
||||
"json_migration.heading.differ": "Uyuşmazlık",
|
||||
"json_migration.heading.extension_list_type": "Uzantı Listesi Türü:",
|
||||
"json_migration.heading.file_extension_list": "Dosya Uzantı Listesi:",
|
||||
"json_migration.heading.match": "Eşleşti",
|
||||
"json_migration.heading.names": "Adlar:",
|
||||
"json_migration.heading.parent_tags": "Üst Etiketler:",
|
||||
|
||||
@@ -152,8 +152,6 @@
|
||||
"json_migration.heading.aliases": "别名:",
|
||||
"json_migration.heading.colors": "颜色:",
|
||||
"json_migration.heading.differ": "差异",
|
||||
"json_migration.heading.extension_list_type": "扩展名列表类型:",
|
||||
"json_migration.heading.file_extension_list": "文件扩展名列表:",
|
||||
"json_migration.heading.match": "已匹配",
|
||||
"json_migration.heading.names": "名字:",
|
||||
"json_migration.heading.parent_tags": "上级标签:",
|
||||
|
||||
@@ -156,8 +156,6 @@
|
||||
"json_migration.heading.aliases": "別名:",
|
||||
"json_migration.heading.colors": "顏色:",
|
||||
"json_migration.heading.differ": "差異",
|
||||
"json_migration.heading.extension_list_type": "副檔名清單類型:",
|
||||
"json_migration.heading.file_extension_list": "檔案副檔名清單:",
|
||||
"json_migration.heading.match": "已一致",
|
||||
"json_migration.heading.names": "名稱:",
|
||||
"json_migration.heading.parent_tags": "父標籤:",
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ def cwd():
|
||||
def file_mediatypes_library():
|
||||
lib = Library()
|
||||
|
||||
status = lib.open_library(Path(""), ":memory:")
|
||||
status = lib.open_library(Path(""), in_memory=True)
|
||||
assert status.success
|
||||
folder = unwrap(lib.folder)
|
||||
|
||||
@@ -84,7 +84,7 @@ def library(request, library_dir: Path): # pyright: ignore
|
||||
library_path = Path(request.param)
|
||||
|
||||
lib = Library()
|
||||
status = lib.open_library(library_path, ":memory:")
|
||||
status = lib.open_library(library_path, in_memory=True)
|
||||
assert status.success
|
||||
folder = unwrap(lib.folder)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
|
||||
from tagstudio.core.enums import LibraryPrefs
|
||||
from tagstudio.core.constants import IGNORE_NAME
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.refresh import RefreshTracker
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
@@ -20,15 +20,14 @@ CWD = Path(__file__).parent
|
||||
def test_refresh_new_files(library: Library, exclude_mode: bool):
|
||||
library_dir = unwrap(library.library_dir)
|
||||
# Given
|
||||
library.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, exclude_mode)
|
||||
library.set_prefs(LibraryPrefs.EXTENSION_LIST, [".md"])
|
||||
registry = RefreshTracker(library=library)
|
||||
library.included_files.clear()
|
||||
(library_dir / "FOO.MD").touch()
|
||||
(library_dir / IGNORE_NAME).write_text("*.md" if exclude_mode else "*\n!*.md")
|
||||
|
||||
# Test if the single file was added
|
||||
list(registry.refresh_dir(library_dir, force_internal_tools=True))
|
||||
assert registry.files_not_in_library == [Path("FOO.MD")]
|
||||
assert set(registry.files_not_in_library) == set([Path(IGNORE_NAME), Path("FOO.MD")])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from tagstudio.qt.mixed.about_modal import AboutModal
|
||||
|
||||
|
||||
def test_github_api_unavailable(qtbot: QtBot, mocker) -> None:
|
||||
mocker.patch(
|
||||
"requests.get",
|
||||
side_effect=ConnectionError(
|
||||
"Failed to resolve 'api.github.com' ([Errno -3] Temporary failure in name resolution)"
|
||||
),
|
||||
)
|
||||
modal = AboutModal("/tmp")
|
||||
qtbot.addWidget(modal)
|
||||
@@ -46,9 +46,9 @@ def test_library_migrations(path: str):
|
||||
try:
|
||||
status = library.open_library(library_dir=temp_path)
|
||||
library.close()
|
||||
shutil.rmtree(temp_path)
|
||||
assert status.success
|
||||
except Exception as e:
|
||||
library.close()
|
||||
shutil.rmtree(temp_path)
|
||||
raise (e)
|
||||
finally:
|
||||
shutil.rmtree(temp_path)
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
|
||||
from tagstudio.core.enums import LibraryPrefs
|
||||
from tagstudio.qt.mixed.migration_modal import JsonMigrationModal
|
||||
|
||||
CWD = Path(__file__)
|
||||
@@ -43,10 +42,4 @@ def test_json_migration():
|
||||
assert modal.check_color_parity()
|
||||
|
||||
# Extension Filter List ====================================================
|
||||
# Count
|
||||
assert len(modal.json_lib.ext_list) == len(modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST))
|
||||
# List Type
|
||||
assert modal.check_ext_type()
|
||||
# No Leading Dot
|
||||
for ext in modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST): # pyright: ignore[reportUnknownVariableType]
|
||||
assert ext[0] != "."
|
||||
modal.assert_ignore_parity()
|
||||
|
||||
@@ -10,7 +10,6 @@ from tempfile import TemporaryDirectory
|
||||
import pytest
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.enums import DefaultEnum, LibraryPrefs
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
FieldID, # pyright: ignore[reportPrivateUsage]
|
||||
@@ -199,11 +198,6 @@ def test_search_library_case_insensitive(library: Library):
|
||||
assert results[0] == entry.id
|
||||
|
||||
|
||||
def test_preferences(library: Library):
|
||||
for pref in LibraryPrefs:
|
||||
assert library.prefs(pref) == pref.default
|
||||
|
||||
|
||||
def test_remove_entry_field(library: Library, entry_full: Entry):
|
||||
title_field = entry_full.text_fields[0]
|
||||
|
||||
@@ -393,24 +387,6 @@ def test_update_field_order(library: Library, entry_full: Entry):
|
||||
assert entry.text_fields[1].value == "second"
|
||||
|
||||
|
||||
def test_library_prefs_multiple_identical_vals():
|
||||
# check the preferences are inherited from DefaultEnum
|
||||
assert issubclass(LibraryPrefs, DefaultEnum)
|
||||
|
||||
# create custom settings with identical values
|
||||
class TestPrefs(DefaultEnum):
|
||||
FOO = 1
|
||||
BAR = 1
|
||||
|
||||
assert TestPrefs.FOO.default == 1
|
||||
assert TestPrefs.BAR.default == 1
|
||||
assert TestPrefs.BAR.name == "BAR"
|
||||
|
||||
# accessing .value should raise exception
|
||||
with pytest.raises(AttributeError):
|
||||
assert TestPrefs.BAR.value
|
||||
|
||||
|
||||
def test_path_search_ilike(library: Library):
|
||||
results = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
|
||||
assert results.total_count == 1
|
||||
|
||||
Reference in New Issue
Block a user