refactor: remove legacy 'search and relink' system, fold into auto-relink

This commit is contained in:
Travis Abendshien
2026-09-10 23:16:09 -07:00
parent 0e7b93aa99
commit e87b32a780
27 changed files with 40 additions and 336 deletions
+1 -97
View File
@@ -9,11 +9,10 @@ from pathlib import Path
from time import time
import structlog
from wcmatch import glob, pathlib
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob
from tagstudio.core.library.ignore import Ignore
from tagstudio.core.library.scanners import scan_paths
from tagstudio.core.utils.filesystem import is_fs_case_sensitive
from tagstudio.core.utils.normalization import norm_path
@@ -36,14 +35,8 @@ class LibrarySyncEngine:
paths_to_restat: list[tuple[int, Path]] = field(default_factory=list)
unlinked_entries: list[Entry] = field(default_factory=list)
relinked_entries: list[Entry] = field(default_factory=list)
manual_relink_count: int = 0
cancelled: bool = False
_scanned_paths: list[Path] = field(default_factory=list, init=False, repr=False)
_filename_to_path_map: dict[Path, list[Path]] | None = field(
default=None, init=False, repr=False
)
@property
def new_file_count(self) -> int:
return len(self.new_paths)
@@ -66,8 +59,6 @@ class LibrarySyncEngine:
self.paths_to_restat = []
self.unlinked_entries = []
self.relinked_entries = []
self._scanned_paths = []
self._filename_to_path_map = None
def _get_case_sensitivity(self) -> bool:
if self.library.is_case_sensitive_fs is None:
@@ -106,7 +97,6 @@ class LibrarySyncEngine:
if self.cancelled:
break
count += 1
self._scanned_paths.append(raw_path)
key = norm_path(raw_path, case_sensitive=case_sensitive)
entry_id = cache.get(key)
if entry_id is not None:
@@ -191,65 +181,6 @@ class LibrarySyncEngine:
index = end
self.paths_to_restat = self.paths_to_restat[index:]
def _build_filename_to_path_map(self, case_sensitive: bool) -> dict[Path, list[Path]]:
index: dict[Path, list[Path]] = {}
for path in self._scanned_paths:
key = norm_path(Path(path.name), case_sensitive=case_sensitive)
index.setdefault(key, []).append(path)
return index
def _glob_for_filename(self, filename: str, case_sensitive: bool) -> list[Path]:
"""Search the library directory for files matching `filename`.
Used only as a fallback when find_relink_candidates() is called without a prior
sync_dir() scan in this engine instance to reuse results from.
"""
library_dir = unwrap(self.library.library_dir)
ignore_patterns = ignore_to_glob(Ignore.get_patterns(library_dir))
target_path = norm_path(Path(filename), case_sensitive=case_sensitive)
flags = PATH_GLOB_FLAGS | (0 if case_sensitive else glob.IGNORECASE)
matches: list[Path] = []
for path in pathlib.Path(str(library_dir)).glob(
patterns=f"***/{glob.escape(filename)}",
flags=flags,
exclude=ignore_patterns,
):
if path.is_dir():
continue
candidate = Path(path).relative_to(library_dir)
if norm_path(Path(candidate.name), case_sensitive=case_sensitive) == target_path:
matches.append(candidate)
return matches
def find_relink_candidates(self, entry: Entry) -> list[Path]:
"""Try to find files in the library directory matching an unlinked entry's filename.
A file already in the path cache can only be a candidate if its normalized path matches
this entry's own normalized path, such as with an NFC/NFD or case only duplicate.
"""
case_sensitive = self._get_case_sensitivity()
target_key = norm_path(Path(entry.path.name), case_sensitive=case_sensitive)
if self._scanned_paths:
if self._filename_to_path_map is None:
self._filename_to_path_map = self._build_filename_to_path_map(case_sensitive)
matches = list(self._filename_to_path_map.get(target_key, []))
else:
matches = self._glob_for_filename(entry.path.name, case_sensitive)
entry_key = norm_path(entry.path, case_sensitive=case_sensitive)
cache = self.library.get_or_build_path_cache()
filtered: list[Path] = []
for path in matches:
path_key = norm_path(path, case_sensitive=case_sensitive)
if path_key == entry_key or cache.get(path_key) is None:
filtered.append(path)
matches = filtered
logger.info("[Sync] Relink candidates", entry=entry.path.as_posix(), matches=matches)
return matches
def _apply_relink(
self, entry: Entry, new_path: Path, cache: dict[Path, int], case_sensitive: bool
) -> bool:
@@ -328,33 +259,6 @@ class LibrarySyncEngine:
return unmatched
def relink_unlinked_entries(self) -> Iterator[int]:
"""Attempt to fix unlinked entries by finding a single matching file in the library."""
self.manual_relink_count = 0
case_sensitive = self._get_case_sensitivity()
cache = self.library.get_or_build_path_cache()
matched: list[Entry] = []
for i, entry in enumerate(self.unlinked_entries):
yield i
candidates = self.find_relink_candidates(entry)
if len(candidates) != 1:
continue
new_path = candidates[0]
if not self._apply_relink(entry, new_path, cache, case_sensitive):
continue
self.manual_relink_count += 1
matched.append(entry)
logger.info(
"[Sync] Relinked entry",
entry=entry.path.as_posix(),
new_path=new_path.as_posix(),
)
for entry in matched:
self.unlinked_entries.remove(entry)
def _merge_duplicate_path_entries(self, case_sensitive: bool, cache: dict[Path, int]) -> None:
"""Merge any entry whose stored path collides with another entry's onto that entry.
@@ -1,37 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from PySide6.QtCore import QObject, Signal
from tagstudio.core.library.sync import LibrarySyncEngine
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.progress_bar import ProgressWidget
class RelinkUnlinkedEntriesProgress(QObject):
done = Signal()
def __init__(self, tracker: LibrarySyncEngine):
super().__init__()
self.tracker = tracker
def repair_entries(self):
def displayed_text(x):
return Translations.format(
"entries.unlinked.relink.attempting",
index=x,
unlinked_count=self.tracker.unlinked_entries_count,
fixed_count=self.tracker.manual_relink_count,
)
pw = ProgressWidget(
label_text="",
cancel_button_text=None,
minimum=0,
maximum=self.tracker.unlinked_entries_count,
)
pw.setWindowTitle(Translations["entries.unlinked.relink.title"])
pw.from_iterable_function(
self.tracker.relink_unlinked_entries, displayed_text, self.done.emit
)
+8 -14
View File
@@ -14,7 +14,6 @@ from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
from tagstudio.qt.controllers.merge_dupe_entries_progress import MergeDuplicateEntriesProgress
from tagstudio.qt.controllers.progress_bar import ProgressWidget
from tagstudio.qt.controllers.relink_entries_progress import RelinkUnlinkedEntriesProgress
from tagstudio.qt.mixed.remove_unlinked_modal import RemoveUnlinkedEntriesModal
from tagstudio.qt.views.styles.stylesheets import header
@@ -40,7 +39,14 @@ class FixUnlinkedEntriesModal(QWidget):
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
self.unlinked_desc_widget = QLabel(Translations["entries.unlinked.description"])
self.unlinked_desc_widget = QLabel(
Translations["entries.unlinked.description"]
+ "<br><br>"
+ Translations["entries.unlinked.description.deleted"]
# TODO: Implement manual relinking
# + "<br><br>"
# + Translations["entries.unlinked.description.ambiguous"]
)
self.unlinked_desc_widget.setObjectName("unlinkedDescriptionLabel")
self.unlinked_desc_widget.setWordWrap(True)
self.unlinked_desc_widget.setStyleSheet("text-align:left;")
@@ -57,16 +63,6 @@ class FixUnlinkedEntriesModal(QWidget):
self.refresh_unlinked_button.clicked.connect(self.refresh_unlinked)
self.merge_class = MergeDuplicateEntriesProgress(self.lib, self.driver)
self.relink_class = RelinkUnlinkedEntriesProgress(self.sync_engine)
self.search_button = QPushButton(Translations["entries.unlinked.search_and_relink"])
self.relink_class.done.connect(
lambda: (
self.driver.update_browsing_state(),
self._sync_ui_from_tracker(),
)
)
self.search_button.clicked.connect(self.relink_class.repair_entries)
self.manual_button = QPushButton(Translations["entries.unlinked.relink.manual"])
self.manual_button.setHidden(True)
@@ -96,7 +92,6 @@ class FixUnlinkedEntriesModal(QWidget):
self.root_layout.addWidget(self.unlinked_count_label)
self.root_layout.addWidget(self.unlinked_desc_widget)
self.root_layout.addWidget(self.refresh_unlinked_button)
self.root_layout.addWidget(self.search_button)
self.root_layout.addWidget(self.manual_button)
self.root_layout.addWidget(self.remove_button)
self.root_layout.addStretch(1)
@@ -152,7 +147,6 @@ class FixUnlinkedEntriesModal(QWidget):
count: int = self.lib.unlinked_entries_count
syncing = self.driver.file_scan_lock # Disabled while a sync is running
self.search_button.setDisabled(count < 1 or syncing)
self.remove_button.setDisabled(count < 1 or syncing)
count_text: str = Translations.format(
@@ -40,7 +40,6 @@
"entries.duplicates.description": "Duplicitní položky jsou definovány jako více položek, které ukazují na stejný soubor na disku. Jejich sloučením se spojí značky a metadata ze všech duplikátů do jediné konsolidované položky. Nesmí se zaměňovat s „duplicitními soubory“, což jsou duplikáty samotných vašich souborů mimo TagStudio.",
"entries.mirror.confirmation": "Opravdu chcete zrcadlit následujících {count} položek?",
"entries.unlinked.relink.manual": "Znovu propojit ručně",
"entries.unlinked.relink.title": "Propojuji záznamy",
"entries.unlinked.scanning": "Skenuji knihovnu pro nepropojené záznamy...",
"entries.unlinked.title": "Opravit nepropojené záznamy",
"field.copy": "Zkopírovat políčko",
+1 -4
View File
@@ -58,14 +58,11 @@
"entries.remove.singular.confirm": "Soll dieser Eintrag von der Bibliothek entfernt werden? Es werden keine Dateien auf der Festplatte gelöscht.",
"entries.running.dialog.new_entries": "Füge {total} neue Dateieinträge hinzu...",
"entries.tags": "Tags",
"entries.unlinked.description": "Jeder Bibliothekseintrag ist mit einer Datei in einem Ihrer Verzeichnisse verknüpft. Wenn eine Datei, die mit einem Eintrag verknüpft ist, außerhalb von TagStudio verschoben oder gelöscht wird, gilt sie als nicht verknüpft.<br><br>Nicht verknüpfte Einträge können durch das Durchsuchen Ihrer Verzeichnisse automatisch neu verknüpft, vom Benutzer manuell neu verknüpft oder auf Wunsch gelöscht werden.",
"entries.unlinked.relink.attempting": "Versuche {index}/{unlinked_count} Einträge neu zu verknüpfen, {fixed_count} bereits erfolgreich neu verknüpft",
"entries.unlinked.description": "Jeder Bibliothekseintrag ist mit einer Datei in einem Ihrer Verzeichnisse verknüpft. Wenn eine Datei, die mit einem Eintrag verknüpft ist, außerhalb von TagStudio verschoben oder gelöscht wird, gilt sie als nicht verknüpft.",
"entries.unlinked.relink.manual": "&Manuell Neuverknüpfen",
"entries.unlinked.relink.title": "Einträge werden neu verknüpft",
"entries.unlinked.remove": "Entferne nicht verknüpfte Einträge",
"entries.unlinked.remove_alt": "Entfer&ne nicht verknüpfte Einträge",
"entries.unlinked.scanning": "Bibliothek wird nach nicht verknüpften Einträgen durchsucht...",
"entries.unlinked.search_and_relink": "&Suchen && Neuverknüpfen",
"entries.unlinked.title": "Unverknüpfte Einträge reparieren",
"entries.unlinked.unlinked_count": "Unverknüpfte Einträge: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -57,14 +57,11 @@
"entries.remove.singular.confirm": "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε αυτή την εγγραφή από τη βιβλιοθήκη σας; Δεν θα διαγραφεί κανένα αρχείο από τον δίσκο.",
"entries.running.dialog.new_entries": "Προσθήκη {total} νέων εγγραφών αρχείων...",
"entries.tags": "Tags",
"entries.unlinked.description": "Κάθε εγγραφή της βιβλιοθήκης είναι συνδεδεμένη με ένα αρχείο σε έναν από τους καταλόγους σας. Εάν ένα αρχείο που είναι συνδεδεμένο με μια εγγραφή μετακινηθεί ή διαγραφεί εκτός του TagStudio, τότε θεωρείται αποσυνδεδεμένο.<br><br>Οι αποσυνδεδεμένες εγγραφές μπορούν να επανασυνδεθούν αυτόματα μέσω αναζήτησης στους καταλόγους σας ή να διαγραφούν, εάν το επιθυμείτε.",
"entries.unlinked.relink.attempting": "Προσπάθεια επανασύνδεσης {index}/{unlinked_count} εγγραφών, {fixed_count} επανασυνδέθηκαν επιτυχώς",
"entries.unlinked.description": "Κάθε εγγραφή της βιβλιοθήκης είναι συνδεδεμένη με ένα αρχείο σε έναν από τους καταλόγους σας. Εάν ένα αρχείο που είναι συνδεδεμένο με μια εγγραφή μετακινηθεί ή διαγραφεί εκτός του TagStudio, τότε θεωρείται αποσυνδεδεμένο.",
"entries.unlinked.relink.manual": "&Χειροκίνητη επανασύνδεση",
"entries.unlinked.relink.title": "Επανασύνδεση εγγραφών",
"entries.unlinked.remove": "Αφαίρεση αποσυνδεδεμένων εγγραφών",
"entries.unlinked.remove_alt": "Α&φαίρεση αποσυνδεδεμένων εγγραφών",
"entries.unlinked.scanning": "Σάρωση βιβλιοθήκης για αποσυνδεδεμένες εγγραφές...",
"entries.unlinked.search_and_relink": "&Αναζήτηση && Επανασύνδεση",
"entries.unlinked.title": "Διόρθωση αποσυνδεδεμένων εγγραφών",
"entries.unlinked.unlinked_count": "Αποσυνδεδεμένες εγγραφές: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+3 -4
View File
@@ -63,15 +63,14 @@
"entries.remove.singular.confirm": "Are you sure you want to remove this entry from your library? No files on disk will be deleted.",
"entries.running.dialog.new_entries": "Adding {total} New File Entries…",
"entries.tags": "Tags",
"entries.unlinked.description": "Each library entry is linked to a file in one of your directories. If a file linked to an entry is moved or deleted outside of TagStudio, it is then considered unlinked.<br><br>Unlinked entries may be automatically relinked via searching your directories or deleted if desired.",
"entries.unlinked.relink.attempting": "Attempting to Relink {index}/{unlinked_count} Entries, {fixed_count} Successfully Relinked",
"entries.unlinked.description": "Unlinked entries are file entries that can no longer find their original file on disk. Most entries are automatically relinked during a library sync, however some cases require manual review.",
"entries.unlinked.description.ambiguous": "For unlinked entries that have ambiguous matches to multiple files in your library, you may manually choose how they get relinked.",
"entries.unlinked.description.deleted": "When you delete files outside of TagStudio, their associated entries become unlinked. You may manually delete any unlinked entries at your own discretion.",
"entries.unlinked.relink.manual": "&Manual Relink",
"entries.unlinked.relink.title": "Relinking Entries",
"entries.unlinked.remove": "Remove Unlinked Entries",
"entries.unlinked.remove_alt": "Remo&ve Unlinked Entries",
"entries.unlinked.review": "Manual &Review",
"entries.unlinked.scanning": "Scanning Library for Unlinked Entries…",
"entries.unlinked.search_and_relink": "&Search && Relink",
"entries.unlinked.title": "Fix Unlinked Entries",
"entries.unlinked.unlinked_count": "Unlinked Entries: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -61,14 +61,11 @@
"entries.remove.singular.confirm": "¿Está seguro que quiere eliminar ésta entrada de su librería? Ningún archivo en el disco será eliminado.",
"entries.running.dialog.new_entries": "Añadiendo {total} nuevas entradas de archivos...",
"entries.tags": "Etiquetas",
"entries.unlinked.description": "Cada entrada de la biblioteca está vinculada a un archivo en uno de tus directorios. Si un archivo vinculado a una entrada se mueve o se elimina fuera de TagStudio, se considerará desvinculado. <br><br>Las entradas no vinculadas se pueden volver a vincular automáticamente mediante una búsqueda en tus directorios, el usuario puede eliminarlas si así lo desea.",
"entries.unlinked.relink.attempting": "Intentando volver a vincular {index}/{unlinked_count} Entradas, {fixed_count} Reenlazado correctamente",
"entries.unlinked.description": "Cada entrada de la biblioteca está vinculada a un archivo en uno de tus directorios. Si un archivo vinculado a una entrada se mueve o se elimina fuera de TagStudio, se considerará desvinculado. ",
"entries.unlinked.relink.manual": "&Reenlace manual",
"entries.unlinked.relink.title": "Volver a vincular las entradas",
"entries.unlinked.remove": "Eliminar Entradas No Vinculadas",
"entries.unlinked.remove_alt": "Quit&ar entradas desvinculadas",
"entries.unlinked.scanning": "Buscando entradas no enlazadas en la biblioteca...",
"entries.unlinked.search_and_relink": "&Buscar && Revincular",
"entries.unlinked.title": "Corregir entradas no vinculadas",
"entries.unlinked.unlinked_count": "Entradas no vinculadas: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -57,14 +57,11 @@
"entries.remove.singular.confirm": "Haluatko varmasti poistaa tämän merkinnän kirjastostasi? Levyllä olevia tiedostoja ei poisteta.",
"entries.running.dialog.new_entries": "Lisätään {total} uutta tiedosto merkintää...",
"entries.tags": "Tunnisteet",
"entries.unlinked.description": "Jokainen kirjastomerkintä on linkitetty tiedostoon jossakin hakemistoistasi. Jos merkintään linkitetty tiedosto siirretään tai poistetaan TagStudion ulkopuolelle, sitä pidetään linkittämättömänä.<br><br>Linkittämättömät merkinnät voidaan linkittää automaattisesti uudelleen hakemistojen haun avulla tai poistaa haluttaessa.",
"entries.unlinked.relink.attempting": "Yritetään linkittää uudelleen {index}/{unlinked_count} merkintää, {fixed_count} uudelleenlinkitys onnistui",
"entries.unlinked.description": "Jokainen kirjastomerkintä on linkitetty tiedostoon jossakin hakemistoistasi. Jos merkintään linkitetty tiedosto siirretään tai poistetaan TagStudion ulkopuolelle, sitä pidetään linkittämättömänä.",
"entries.unlinked.relink.manual": "&Manual Relink",
"entries.unlinked.relink.title": "Uudelleen yhdistetään merkintöjä",
"entries.unlinked.remove": "Poista linkittämättömät merkinnät",
"entries.unlinked.remove_alt": "Remo&ve Unlinked Entries",
"entries.unlinked.scanning": "Skannataan kirjastosta linkittämättömiä merkintöjä...",
"entries.unlinked.search_and_relink": "&Search && Relink",
"entries.unlinked.title": "Korjaa linkittämättömät merkinnät",
"entries.unlinked.unlinked_count": "Linkittämättömät merkinnät: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
@@ -48,12 +48,9 @@
"entries.remove.plural.confirm": "Sigurado ka ba gusto mong burahin ang (mga) sumusunod na {count} entry?",
"entries.running.dialog.new_entries": "Dinadagdag ang {total} Mga Bagong Entry ng File…",
"entries.tags": "Mga Tag",
"entries.unlinked.description": "Ang bawat entry sa library ay naka-link sa isang file sa isa sa iyong mga direktoryo. Kung ang isang file na naka-link sa isang entry ay inilipat o binura sa labas ng TagStudio, ito ay isinasaalang-alang na naka-unlink.<br><br>Ang mga naka-unlink na entry ay maaring i-link muli sa pamamagitan ng paghahanap sa iyong mga direktoryo o buburahin kung ninanais.",
"entries.unlinked.relink.attempting": "Sinusubukang i-link muli ang {index}/{unlinked_count} Mga Entry, {fixed_count} Matagumpay na na-link muli",
"entries.unlinked.description": "Ang bawat entry sa library ay naka-link sa isang file sa isa sa iyong mga direktoryo. Kung ang isang file na naka-link sa isang entry ay inilipat o binura sa labas ng TagStudio, ito ay isinasaalang-alang na naka-unlink.",
"entries.unlinked.relink.manual": "&Manwal na Pag-link Muli",
"entries.unlinked.relink.title": "Nili-link muli ang Mga Entry",
"entries.unlinked.scanning": "Sina-scan ang Library para sa Mga Naka-unlink na Entry…",
"entries.unlinked.search_and_relink": "&Maghanap at Mag-link muli",
"entries.unlinked.title": "Ayusin ang Mga Naka-unlink na Entry",
"entries.unlinked.unlinked_count": "Mga Naka-unlink na Entry: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -62,14 +62,11 @@
"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.tags": "Tags",
"entries.unlinked.description": "Chaque entrée dans la bibliothèque est liée à un fichier dans l'un de vos dossiers. Si un fichier lié à une entrée est déplacé ou supprimé en dehors de TagStudio, il est alors considéré non lié. <br><br>Les entrées non liées peuvent être automatiquement reliées via la recherche dans vos dossiers, reliées manuellement par l'utilisateur, ou supprimées si désiré.",
"entries.unlinked.relink.attempting": "Tentative de Reliage de {index}/{unlinked_count} Entrées, {fixed_count} ont été Reliées avec Succès",
"entries.unlinked.description": "Chaque entrée dans la bibliothèque est liée à un fichier dans l'un de vos dossiers. Si un fichier lié à une entrée est déplacé ou supprimé en dehors de TagStudio, il est alors considéré non lié. ",
"entries.unlinked.relink.manual": "&Reliage Manuel",
"entries.unlinked.relink.title": "Reliage des Entrées",
"entries.unlinked.remove": "Supprimer les entrées non liées",
"entries.unlinked.remove_alt": "Supprim&er les entrées non liées",
"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}",
"ffmpeg.missing.status": "{ffmpeg} : {ffmpeg_status}<br>{ffprobe} : {ffprobe_status}",
+1 -4
View File
@@ -62,14 +62,11 @@
"entries.remove.singular.confirm": "Biztosan el akarja távolítani ezt az elemet a könyvtárból? A lemezen található fájl nem lesz törölve.",
"entries.running.dialog.new_entries": "{total} új elem felvétele folyamatban…",
"entries.tags": "Címkék",
"entries.unlinked.description": "A könyvtár minden eleme egy fájllal van összekapcsolva a számítógépen. Ha egy kapcsolt fájl a TagSudión kívül áthelyezésre vagy törésre kerül, akkor ez a kapcsolat megszakad.<br><br>Ezeket a kapcsolat nélküli elemeket a program megpróbálhatja automatikusan megkeresni, de Ön is kézileg újra összekapcsolhatja vagy törölheti őket.",
"entries.unlinked.relink.attempting": "{unlinked_count}/{index} elem újra összekapcsolásának megkísérlése; {fixed_count} elem sikeresen újra összekapcsolva",
"entries.unlinked.description": "A könyvtár minden eleme egy fájllal van összekapcsolva a számítógépen. Ha egy kapcsolt fájl a TagSudión kívül áthelyezésre vagy törésre kerül, akkor ez a kapcsolat megszakad.",
"entries.unlinked.relink.manual": "Új&ra összekapcsolás kézileg",
"entries.unlinked.relink.title": "Elemek újra összekapcsolása",
"entries.unlinked.remove": "Kapcsolat nélküli elemek eltávolítása",
"entries.unlinked.remove_alt": "&Kapcsolat nélküli elemek eltávolítása",
"entries.unlinked.scanning": "Kapcsolat nélküli elemek keresése a könyvtárban…",
"entries.unlinked.search_and_relink": "&Keresés és újra összekapcsolás",
"entries.unlinked.title": "Kapcsolat nélküli elemek javítása",
"entries.unlinked.unlinked_count": "Kapcsolat nélküli elemek: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -57,14 +57,11 @@
"entries.remove.singular.confirm": "Sei sicuro di voler rimuovere questa voce dalla tua biblioteca? Nessun file su disco verrà eliminato.",
"entries.running.dialog.new_entries": "Aggiundendo {total} Nuove Voci di File...",
"entries.tags": "Etichette",
"entries.unlinked.description": "Ogni voce della biblioteca è collegata ad un file in una delle tue cartelle. Se un file collegato ad una voce viene spostato o eliminito al di fuori di TagStudio, la voce corrispondente viene considerata scollegata.<br><br>Le voci scollegate possono essere ricollegate automaticamente attraverso la ricerca nelle tue cartelle oppure cancellate se lo si desidera.",
"entries.unlinked.relink.attempting": "Tentativo di Ricollegare {index}/{unlinked_count} Voci, {fixed_count} Ricollegate con Successo",
"entries.unlinked.description": "Ogni voce della biblioteca è collegata ad un file in una delle tue cartelle. Se un file collegato ad una voce viene spostato o eliminito al di fuori di TagStudio, la voce corrispondente viene considerata scollegata.",
"entries.unlinked.relink.manual": "Ricollegamento &Manuale",
"entries.unlinked.relink.title": "Ricollegamento Voci",
"entries.unlinked.remove": "Rimuovi Voci non Collegate",
"entries.unlinked.remove_alt": "Rimuo&vi Voci non Collegate",
"entries.unlinked.scanning": "Scansionando la Biblioteca in cerca di Voci non Collegate...",
"entries.unlinked.search_and_relink": "&Ricerca && Ricollega",
"entries.unlinked.title": "Correggi Voci non Collegate",
"entries.unlinked.unlinked_count": "Voci non Collegate: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -62,14 +62,11 @@
"entries.remove.singular.confirm": "このエントリをライブラリから削除しますか? ディスク上のファイルは削除されません。",
"entries.running.dialog.new_entries": "{total} 件の新しいファイル エントリを追加しています…",
"entries.tags": "タグ",
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルが TagStudio 以外で移動または削除された場合、そのエントリはリンク切れとして扱われます。<br><br>リンク切れのエントリは、ディレクトリを検索して自動的に再リンクすることも、必要に応じて削除することもできます。",
"entries.unlinked.relink.attempting": "{unlinked_count} 件中 {index} 件のエントリを再リンク中、{fixed_count} 件を正常に再リンクしました",
"entries.unlinked.description": "ライブラリの各エントリは、ディレクトリ内のファイルにリンクされています。エントリにリンクされたファイルが TagStudio 以外で移動または削除された場合、そのエントリはリンク切れとして扱われます。",
"entries.unlinked.relink.manual": "手動で再リンク(&M)",
"entries.unlinked.relink.title": "エントリの再リンク",
"entries.unlinked.remove": "リンク切れのエントリを削除",
"entries.unlinked.remove_alt": "リンク切れのエントリを削除(&V)",
"entries.unlinked.scanning": "リンク切れのエントリをライブラリ内でスキャンしています…",
"entries.unlinked.search_and_relink": "検索して再リンク(&S)",
"entries.unlinked.title": "リンク切れのエントリを修正",
"entries.unlinked.unlinked_count": "リンク切れのエントリ数: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
@@ -56,12 +56,9 @@
"entries.remove.singular.confirm": "Er du sikker på at du vil fjerne denne oppføringen fra bibliotek ditt? Ingen filer på disken vil slettes.",
"entries.running.dialog.new_entries": "Legger til {total} Nye Filoppføringer...",
"entries.tags": "Etiketter",
"entries.unlinked.description": "Hver biblioteksoppføring er koblet til en fil i en av dine mapper. Hvis en fil koblet til en oppføring er flyttet eller slettet utenfor TagStudio, så er den sett på som frakoblet.<br><br>Frakoblede oppføringer kan bli automatisk gjenkoblet ved å søke i mappene dine eller slettet om det er ønsket.",
"entries.unlinked.relink.attempting": "Forsøker å Gjenkoble {index}/{unlinked_count} Oppføringer, {fixed_count} Klart Gjenkoblet",
"entries.unlinked.description": "Hver biblioteksoppføring er koblet til en fil i en av dine mapper. Hvis en fil koblet til en oppføring er flyttet eller slettet utenfor TagStudio, så er den sett på som frakoblet.",
"entries.unlinked.relink.manual": "&Manuell Gjenkobling",
"entries.unlinked.relink.title": "Gjenkobler Oppføringer",
"entries.unlinked.scanning": "Skanner bibliotek for ulenkede oppføringer …",
"entries.unlinked.search_and_relink": "&Søk && Gjenkobl",
"entries.unlinked.title": "Fiks ulenkede oppføringer",
"entries.unlinked.unlinked_count": "Frakoblede Oppføringer: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -48,12 +48,9 @@
"entries.remove.plural.confirm": "Jesteś pewien że chcesz usunąć następujące {count} wpisy?",
"entries.running.dialog.new_entries": "Dodawanie {total} nowych wpisów plików...",
"entries.tags": "Tagi",
"entries.unlinked.description": "Każdy wpis w bibliotece jest połączony z plikiem w jednym z twoich katalogów. Jeśli połączony plik jest przeniesiony poza TagStudio albo usunięty to jest uważany za odłączony.<br><br>Odłączone wpisy mogą być automatycznie połączone ponownie przez szukanie twoich katalogów, ręczne ponowne łączenie przez użytkownika lub usunięte jeśli zajdzie taka potrzeba.",
"entries.unlinked.relink.attempting": "Próbowanie ponownego łączenia {index}/{unlinked_count} wpisów, {fixed_count} poprawnie połączono ponownie",
"entries.unlinked.description": "Każdy wpis w bibliotece jest połączony z plikiem w jednym z twoich katalogów. Jeśli połączony plik jest przeniesiony poza TagStudio albo usunięty to jest uważany za odłączony.",
"entries.unlinked.relink.manual": "&Ręczne ponowne łączenie",
"entries.unlinked.relink.title": "Ponowne łączenie wpisów",
"entries.unlinked.scanning": "Skanowanie biblioteki dla odłączonych wpisów...",
"entries.unlinked.search_and_relink": "&Wyszukaj && Zalinkuj ponownie",
"entries.unlinked.title": "Napraw odłączone wpisy",
"entries.unlinked.unlinked_count": "Odłączone wpisy: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -53,13 +53,10 @@
"entries.remove.plural.confirm": "Tem certeza que deseja apagar os seguintes {count} registos ?",
"entries.running.dialog.new_entries": "A Adicionar {total} Novos Registos de Ficheiros...",
"entries.tags": "Tags",
"entries.unlinked.description": "Cada registo na biblioteca faz referência à um ficheiro numa das suas pastas. Se um ficheiro referenciado à uma entrada for movido ou apagado fora do TagStudio, ele é depois considerado não-referenciado.<br><br>Registos não-referenciados podem ser automaticamente referenciados por pesquisas nos seus diretórios, manualmente pelo utilizador, ou apagado se for desejado.",
"entries.unlinked.relink.attempting": "A tentar referenciar {index}/{unlinked_count} Registos, {fixed_count} Referenciados com Sucesso",
"entries.unlinked.description": "Cada registo na biblioteca faz referência à um ficheiro numa das suas pastas. Se um ficheiro referenciado à uma entrada for movido ou apagado fora do TagStudio, ele é depois considerado não-referenciado.",
"entries.unlinked.relink.manual": "&Referência Manual",
"entries.unlinked.relink.title": "A Referenciar Registos",
"entries.unlinked.remove_alt": "Remover Entradas sem Conexões",
"entries.unlinked.scanning": "A escanear biblioteca por registos não referenciados...",
"entries.unlinked.search_and_relink": "&Pesquisar && Referenciar",
"entries.unlinked.title": "Corrigir Registos Não Referenciados",
"entries.unlinked.unlinked_count": "Registos Não Referenciados: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
@@ -57,14 +57,11 @@
"entries.remove.singular.confirm": "Você tem certeza que deseja remover esse registro da sua bilbioteca ? Nenhum arquivo no disco será excluído.",
"entries.running.dialog.new_entries": "Adicionando {total} Novos Registros de Arquivos...",
"entries.tags": "Tags",
"entries.unlinked.description": "Cada registro na biblioteca faz referência à um arquivo em uma de suas pastas. Se um arquivo referenciado à uma entrada for movido ou deletado fora do TagStudio, ele é então considerado não-referenciado.<br><br>Registros não-referenciados podem ser automaticamente referenciados por buscas nos seus diretórios, manualmente pelo usuário, ou deletado se for desejado.",
"entries.unlinked.relink.attempting": "Tentando referenciar {index}/{unlinked_count} Registros, {fixed_count} Referenciados com Sucesso",
"entries.unlinked.description": "Cada registro na biblioteca faz referência à um arquivo em uma de suas pastas. Se um arquivo referenciado à uma entrada for movido ou deletado fora do TagStudio, ele é então considerado não-referenciado.",
"entries.unlinked.relink.manual": "&Referência Manual",
"entries.unlinked.relink.title": "Referenciando Registros",
"entries.unlinked.remove": "Remover Registros Não Vinculados",
"entries.unlinked.remove_alt": "Remover Entradas sem Conexões",
"entries.unlinked.scanning": "Escaneando bibliotecada em busca de registros não referenciados...",
"entries.unlinked.search_and_relink": "&Buscar && Referenciar",
"entries.unlinked.title": "Corrigir Registros Não Referenciados",
"entries.unlinked.unlinked_count": "Registros Não Referenciados: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
@@ -57,14 +57,11 @@
"entries.remove.singular.confirm": "Du kestetsa afto shiruzmakaban long mlafuhuomi we? Nil mlafu na shiruzmabaksu bli kestejena.",
"entries.running.dialog.new_entries": "Nasii {total} neo shiruzmakaban fu mlafu ima...",
"entries.tags": "Festaretol",
"entries.unlinked.description": "Tont shiruzmakaban fu mlafuhuomi tsunagajena na mlafu ine joku mlafukaban fu du. Li mlafu tsunagajena na shiruzmakaban ugokijena os kestejena ekso TagStudio, sit sore kntsunagajena.<br><br>Tsunaganaijena shiruzmakaban deki tsunaga gen na suha per mlafukaban fu du os keste li du vil.",
"entries.unlinked.relink.attempting": "Iskat ima na tsunaga gen {index}/{unlinked_count} shiruzmakaban, {fixed_count} tsunagajena gen",
"entries.unlinked.description": "Tont shiruzmakaban fu mlafuhuomi tsunagajena na mlafu ine joku mlafukaban fu du. Li mlafu tsunagajena na shiruzmakaban ugokijena os kestejena ekso TagStudio, sit sore kntsunagajena.",
"entries.unlinked.relink.manual": "&Tsunaga gen mit hant",
"entries.unlinked.relink.title": "Tsunaga shiruzmakaban gen",
"entries.unlinked.remove": "Keste kntsunagajena shiruzmakaban",
"entries.unlinked.remove_alt": "Keste kntsunagajena shiruzmakaban (&v)",
"entries.unlinked.scanning": "Taskame mlafuhuomi ima grun kntsunagajena shiruzmakaban...",
"entries.unlinked.search_and_relink": "&Suha &&Tsunaga gen",
"entries.unlinked.title": "Fiks kntsunagajena shiruzmakaban",
"entries.unlinked.unlinked_count": "Kntsunagajena shiruzmakaban: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -59,13 +59,10 @@
"entries.remove.singular.confirm": "Вы уверены, что хотите удалить эту запись? Файл на диске не будет удалён.",
"entries.running.dialog.new_entries": "Добавление {total} новых записей...",
"entries.tags": "Теги",
"entries.unlinked.description": "Каждая запись в библиотеке привязана к файлу, находящегося внутри той или иной папки. Если файл, к которому была привязана запись, был удалён или перемещён без использования TagStudio, то запись становиться \"откреплённой\".<br><br>Откреплённые записи могут быть прикреплены обратно автоматически, либо же удалены если в них нет надобности.",
"entries.unlinked.relink.attempting": "Попытка перепривязать {index}/{unlinked_count} записей, {fixed_count} привязано успешно",
"entries.unlinked.description": "Каждая запись в библиотеке привязана к файлу, находящегося внутри той или иной папки. Если файл, к которому была привязана запись, был удалён или перемещён без использования TagStudio, то запись становиться \"откреплённой\".",
"entries.unlinked.relink.manual": "&Ручная привязка",
"entries.unlinked.relink.title": "Привязка записей",
"entries.unlinked.remove": "Удалить откреплённые записи",
"entries.unlinked.scanning": "Сканирование библиотеки на наличие откреплённых записей...",
"entries.unlinked.search_and_relink": "&Поиск и привязка",
"entries.unlinked.title": "Исправить откреплённые записи",
"entries.unlinked.unlinked_count": "Откреплённых записей: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
@@ -58,13 +58,10 @@
"entries.running.dialog.new_entries": "Lägger Till {total} Nya Filposter...",
"entries.tags": "Etiketter",
"entries.unlinked.description": "Varje post i biblioteket är länkad till en fil i en av dina kataloger. Om en fil länkad till en post är flyttad eller borttagen utanför TagStudio blir den olänkad. Olänkade poster kan automatiskt bli omlänkade genom att söka genom dina kataloger, manuellt omlänkade av användaren eller tas bort om så önskas.",
"entries.unlinked.relink.attempting": "Försöker att länka om {index}/{unlinked_count} Poster, {fixed_count} Lyckades Länkas Om",
"entries.unlinked.relink.manual": "Länka om manuellt",
"entries.unlinked.relink.title": "Länkar om poster",
"entries.unlinked.remove": "Ta Bort Olänkade Poster",
"entries.unlinked.remove_alt": "&Ta Bort Olänkade Poster",
"entries.unlinked.scanning": "Skannar bibliotek efter olänkade poster...",
"entries.unlinked.search_and_relink": "Sök && Länka om",
"entries.unlinked.title": "Fixa olänkade poster",
"entries.unlinked.unlinked_count": "Olänkade Poster: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
@@ -58,13 +58,10 @@
"entries.running.dialog.new_entries": "{total} புதிய கோப்பு உள்ளீடுகளைச் சேர்ப்பது ...",
"entries.tags": "குறிச்சொற்கள்",
"entries.unlinked.description": "ஒவ்வொரு நூலக நுழைவும் உங்கள் கோப்பகங்களில் ஒன்றில் ஒரு கோப்போடு இணைக்கப்பட்டுள்ளது. ஒரு நுழைவுடன் இணைக்கப்பட்ட ஒரு கோப்பு முகவரிச்சீட்டுஅறைக்கு வெளியே நகர்த்தப்பட்டால் அல்லது நீக்கப்பட்டால், அது பின்னர் இணைக்கப்படாததாகக் கருதப்படுகிறது.",
"entries.unlinked.relink.attempting": "{index}/{unlinked_count} உள்ளீடுகளை மீண்டும் இணைக்க முயற்சிக்கிறது, {fixed_count} மீண்டும் இணைக்கப்பட்டது",
"entries.unlinked.relink.manual": "& கையேடு மறுபரிசீலனை",
"entries.unlinked.relink.title": "உள்ளீடுகள் மீண்டும் இணைக்கப்படுகின்றது",
"entries.unlinked.remove": "இணைக்கப்படாத உள்ளீடுகளை அகற்று",
"entries.unlinked.remove_alt": "இணைக்கப்படாத உள்ளீடுகளை அகற்று&விடு",
"entries.unlinked.scanning": "இணைக்கப்படாத நுழைவுகளை புத்தககல்லரியில் சோதனை செய்யப்படுகிறது...",
"entries.unlinked.search_and_relink": "& தேடல் && relink",
"entries.unlinked.title": "இணைக்கப்படாத உள்ளீடுகளைச் சரிசெய்யவும்",
"entries.unlinked.unlinked_count": "இணைக்கப்படாத உள்ளீடுகள்: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status} <br> {ffprobe}: {ffprobe_status}",
@@ -56,14 +56,11 @@
"entries.remove.singular.confirm": "mi weka e ijo ni. ni li pona anu seme? poki lipu pi ilo sina la lipu ala li weka.",
"entries.running.dialog.new_entries": "mi pana e lipu sin {total}...",
"entries.tags": "poki",
"entries.unlinked.description": "ijo ale li jo e ijo lon tomo sina. ona li tawa anu weka lon ilo TagStudio ala la, ona li jo ala e ijo lon.<br><br>ijo pi ijo lon li ken alasa lon tomo li ken kama jo e ijo lon. ante la sina ken weka e ona.",
"entries.unlinked.relink.attempting": "mi o pana e ijo lon tawa ijo {index}/{unlinked_count}. mi pana e ijo lon tawa ijo {fixed_count}",
"entries.unlinked.description": "ijo ale li jo e ijo lon tomo sina. ona li tawa anu weka lon ilo TagStudio ala la, ona li jo ala e ijo lon.",
"entries.unlinked.relink.manual": "sina o pana e ijo lon tawa ijo (&M)",
"entries.unlinked.relink.title": "mi pana e ijo lon tawa ijo",
"entries.unlinked.remove": "o weka e ijo pi ijo lon ala",
"entries.unlinked.remove_alt": "o weka e ijo pi ijo lon ala (&V)",
"entries.unlinked.scanning": "mi o alasa e ijo pi ijo lon ala...",
"entries.unlinked.search_and_relink": "o ala&sa o pana e ijo lon tawa ijo",
"entries.unlinked.title": "o pona e ijo pi ijo lon ala",
"entries.unlinked.unlinked_count": "ijo pi ijo lon ala: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+1 -4
View File
@@ -47,12 +47,9 @@
"entries.remove.plural.confirm": "{count} tane kayıtları silmek istediğinden emin misin?",
"entries.running.dialog.new_entries": "{total} Yeni Dosya Kaydı Ekleniyor...",
"entries.tags": "Etiketler",
"entries.unlinked.description": "Kütüphanenizdeki her bir kayıt, dizinlerinizden bir tane dosya ile eşleştirilmektedir. Eğer bir kayıta bağlı dosya TagStudio dışında taşınır veya silinirse, o dosya artık kopmuş olarak sayılır.<br><br>Kopmuş kayıtlar dizinlerinizde arama yapılırken otomatik olarak tekrar eşleştirilebilir, manuel olarak sizin tarafınızdan eşleştirilebilir veya isteğiniz üzere silinebilir.",
"entries.unlinked.relink.attempting": "{index}/{unlinked_count} Kayıt Yeniden Eşleştirilmeye Çalışılıyor, {fixed_count} Başarıyla Yeniden Eşleştirildi",
"entries.unlinked.description": "Kütüphanenizdeki her bir kayıt, dizinlerinizden bir tane dosya ile eşleştirilmektedir. Eğer bir kayıta bağlı dosya TagStudio dışında taşınır veya silinirse, o dosya artık kopmuş olarak sayılır.",
"entries.unlinked.relink.manual": "&Manuel Yeniden Eşleştirme",
"entries.unlinked.relink.title": "Kayıtlar Yeniden Eşleştiriliyor",
"entries.unlinked.scanning": "Kütüphane, Kopmuş Kayıtlar için Taranıyor...",
"entries.unlinked.search_and_relink": "&Ara && Yeniden Eşleştir",
"entries.unlinked.title": "Kopmuş Kayıtları Düzelt",
"entries.unlinked.unlinked_count": "Kopmuş Kayıtlar: {count}",
"field.add": "Ek Bilgi Ekle",
@@ -56,14 +56,11 @@
"entries.remove.plural.confirm": "您确定要删除以下 {count} 个项目?",
"entries.running.dialog.new_entries": "正在加入 {total} 个新文件项目...",
"entries.tags": "标签",
"entries.unlinked.description": "每个仓库条目都链接到一个目录中的文件。如果链接到某个条目的文件在TagStudio之外被移动或删除,则会被视为未链接。<br><br>未链接的条目可能会通过搜索目录自动重新链接,或者根据需要删除。",
"entries.unlinked.relink.attempting": "正在尝试重新链接 {index}/{unlinked_count} 个项目, {fixed_count} 个项目成功重链",
"entries.unlinked.description": "每个仓库条目都链接到一个目录中的文件。如果链接到某个条目的文件在TagStudio之外被移动或删除,则会被视为未链接。",
"entries.unlinked.relink.manual": "手动重新链接(&m)",
"entries.unlinked.relink.title": "正在重新链接项目",
"entries.unlinked.remove": "删除未链接项目",
"entries.unlinked.remove_alt": "删除未链接项目(&v)",
"entries.unlinked.scanning": "正在扫描仓库以寻找未链接的项目...",
"entries.unlinked.search_and_relink": "搜索并重新链接(&s)",
"entries.unlinked.title": "修复未链接的项目",
"entries.unlinked.unlinked_count": "未链接的项目: {count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
@@ -57,14 +57,11 @@
"entries.remove.singular.confirm": "您確定要從您的文件庫刪除這個項目嗎? 硬碟上不會有檔案被刪除。",
"entries.running.dialog.new_entries": "正在加入 {total} 個新檔案項目...",
"entries.tags": "標籤",
"entries.unlinked.description": "每個文件庫的項目都連接到您的其中一個檔案,如果一個已連接的檔案被刪除或移出 TagStudio,那麼這個項目會被歸類為「未連接」。<br><br>您可以透過搜尋您的檔案來讓未連接的項目自動重新連接,或者自動刪除這些未連接項目。",
"entries.unlinked.relink.attempting": "正在嘗試重新連接 {index}/{unlinked_count} 個項目,已成功重新連接 {fixed_count} 個",
"entries.unlinked.description": "每個文件庫的項目都連接到您的其中一個檔案,如果一個已連接的檔案被刪除或移出 TagStudio,那麼這個項目會被歸類為「未連接」。",
"entries.unlinked.relink.manual": "手動重新連接 (&M)",
"entries.unlinked.relink.title": "正在重新連接",
"entries.unlinked.remove": "刪除未連接項目",
"entries.unlinked.remove_alt": "刪除未連接項目 (&V)",
"entries.unlinked.scanning": "正在掃描文件庫中的未連接項目...",
"entries.unlinked.search_and_relink": "搜尋並重新連接 (&S)",
"entries.unlinked.title": "修復未連接項目",
"entries.unlinked.unlinked_count": "未連接項目:{count}",
"ffmpeg.missing.status": "{ffmpeg}: {ffmpeg_status}<br>{ffprobe}: {ffprobe_status}",
+9 -101
View File
@@ -153,99 +153,6 @@ def test_sync_duplicate_case_collision_merged(library: Library):
assert library.duplicate_path_entry_ids == []
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
def test_sync_unlinked_entries(library: Library):
"""`relink_unlinked_entries()` must relink an entry to its one matching, unclaimed file."""
library_dir = unwrap(library.library_dir)
engine = LibrarySyncEngine(library=library)
(library_dir / "sub").mkdir()
(library_dir / "sub" / "found.txt").touch()
library.add_entries([Entry(path=Path("found.txt"), fields=[])])
entry_id = library.get_entry_id_from_path(Path("found.txt"))
assert entry_id >= 0
# Skip sync_dir() to avoid a scan, and manually create unlinked entries to test
engine.unlinked_entries = [unwrap(library.get_entry_full(entry_id))]
list(engine.relink_unlinked_entries())
assert engine.manual_relink_count == 1
assert engine.unlinked_entries == []
assert library.get_entry_id_from_path(Path("sub/found.txt")) == entry_id
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
def test_sync_relink_ignores_files_already_linked_elsewhere(library: Library):
"""`find_relink_candidates()` must not offer a file linked elsewhere at a different path."""
library_dir = unwrap(library.library_dir)
engine = LibrarySyncEngine(library=library)
(library_dir / "kept").mkdir()
(library_dir / "kept" / "target.txt").touch()
list(engine.sync_dir(library_dir, force_internal_scanner=True))
list(engine.save_new_entries())
kept_id = library.get_entry_id_from_path(Path("kept/target.txt"))
assert kept_id >= 0
# A different, unrelated entry that happens to share a filename with the kept file above
library.add_entries([Entry(path=Path("old/target.txt"), fields=[])])
orphan_id = library.get_entry_id_from_path(Path("old/target.txt"))
# Skip sync_dir() to avoid a scan, and manually create unlinked entries to test
engine.unlinked_entries = [unwrap(library.get_entry_full(orphan_id))]
list(engine.relink_unlinked_entries())
assert engine.manual_relink_count == 0
assert len(engine.unlinked_entries) == 1
# The kept entry must be untouched, no merge should have occurred
assert library.get_entry_id_from_path(Path("kept/target.txt")) == kept_id
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
def test_sync_relink_merges_nfc_nfd_duplicate(library: Library):
"""`relink_unlinked_entries()` must merge a duplicate entry differing only by Unicode form.
PathType always normalizes to NFD, so this test uses raw SQL to simulate legacy data
from before that rule existed.
"""
library_dir = unwrap(library.library_dir)
engine = LibrarySyncEngine(library=library)
nfc_name = unicodedata.normalize("NFC", "SKÅL.txt")
nfd_name = unicodedata.normalize("NFD", "SKÅL.txt")
assert nfc_name != nfd_name
# The real file lands in NFD form, as most filesystems store it regardless of input form
(library_dir / nfd_name).touch()
entry_a_id, entry_b_id = library.add_entries(
[
Entry(path=Path(nfd_name), fields=[]),
Entry(path=Path("placeholder.txt"), fields=[]),
]
)
with Session(library.engine) as session:
session.execute(
text("UPDATE entries SET path = :path WHERE id = :id"),
{"path": nfc_name, "id": entry_b_id},
)
session.commit()
library.path_cache = None # Force a rebuild to pick up the raw SQL change
library.get_or_build_path_cache()
assert library.duplicate_path_entry_ids is not None
assert len(library.duplicate_path_entry_ids) == 1
dupe_id = library.duplicate_path_entry_ids[0]
kept_id = entry_b_id if dupe_id == entry_a_id else entry_a_id
# Skip sync_dir() to isolate manual relink
engine.unlinked_entries = [unwrap(library.get_entry_full(dupe_id))]
entries_before = library.entries_count
list(engine.relink_unlinked_entries())
assert engine.manual_relink_count == 1
assert dupe_id not in {e.id for e in engine.unlinked_entries}
# A merge deletes the duplicate outright, rather than just reassigning its path
assert library.entries_count == entries_before - 1
assert unwrap(library.get_entry_full(kept_id)).id == kept_id
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
def test_sync_auto_relink_merges_nfc_nfd_duplicate(library: Library):
"""A duplicate entry differing only by Unicode form must be merged automatically.
@@ -292,8 +199,8 @@ def test_sync_auto_relink_merges_nfc_nfd_duplicate(library: Library):
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
def test_sync_case_sensitivity_aware_relink(library: Library):
"""`find_relink_candidates()` must respect the library's case-sensitivity setting."""
def test_sync_auto_relink_respects_case_sensitivity(library: Library):
"""The filename-only fallback pass must respect the library's case-sensitivity setting."""
library_dir = unwrap(library.library_dir)
engine = LibrarySyncEngine(library=library)
@@ -301,15 +208,16 @@ def test_sync_case_sensitivity_aware_relink(library: Library):
(library_dir / "Other" / "name.txt").touch()
library.add_entries([Entry(path=Path("Folder/Name.txt"), fields=[])])
list(engine.sync_dir(library_dir, force_internal_scanner=True))
unlinked_entry = next(e for e in engine.unlinked_entries if e.path == Path("Folder/Name.txt"))
library.is_case_sensitive_fs = True
assert engine.find_relink_candidates(unlinked_entry) == []
list(engine.sync_dir(library_dir, force_internal_scanner=True))
assert engine.relinked_entries_count == 0
assert Path("Folder/Name.txt") in {e.path for e in engine.unlinked_entries}
library.is_case_sensitive_fs = False
engine._filename_to_path_map = None # Rebuild the index with the new case sensitivity
assert engine.find_relink_candidates(unlinked_entry) == [Path("Other/name.txt")]
list(engine.sync_dir(library_dir, force_internal_scanner=True))
assert engine.relinked_entries_count == 1
assert engine.relinked_entries[0].path == Path("Folder/Name.txt")
assert library.get_entry_id_from_path(Path("Other/name.txt")) >= 0
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)