diff --git a/docs/cli-refresh.md b/docs/cli-refresh.md new file mode 100644 index 000000000..fb237c8f3 --- /dev/null +++ b/docs/cli-refresh.md @@ -0,0 +1,77 @@ +# Command-Line Library Refresh + +## Overview + +TagStudio now supports refreshing libraries from the command line without launching the GUI. This is particularly useful for setting up automated background refreshes on large libraries. + +## Usage + +### Basic Syntax + +```bash +tagstudio --refresh /path/to/library +``` + +Or using the short form: + +```bash +tagstudio -r /path/to/library +``` + +### Examples + +#### Refresh a library on your Desktop + +```bash +tagstudio --refresh ~/Desktop/my-media-library +``` + +#### Refresh a library and capture the output + +```bash +tagstudio --refresh /mnt/large-drive/photos/ > refresh.log +``` + +#### Set up automatic background refresh (Linux/macOS) + +Using cron to refresh a library every night at 2 AM: + +```bash +0 2 * * * /usr/local/bin/tagstudio --refresh ~/media/library +``` + +#### Set up automatic background refresh (Windows) + +Using Task Scheduler: + +1. Create a new basic task +2. Set the trigger to your desired time +3. Set the action to: `C:\path\to\python.exe -m tagstudio.main -r C:\path\to\library` + +## Output + +The command will display the following information upon completion: + +``` +Refresh complete: scanned 5000 files, added 25 new entries +``` + +The exit code will be: + +- `0` if the refresh completed successfully +- `1` if an error occurred (invalid path, corrupted library, etc.) + +## Error Handling + +If an error occurs, the command will display an error message and exit with code 1. Common errors include: + +- **Library path does not exist**: Verify the path is correct and accessible +- **Failed to open library**: The library may be corrupted or not a valid TagStudio library +- **Library requires JSON to SQLite migration**: Open the library in the GUI to complete the migration + +## Notes + +- The refresh process scans the library directory for new files that are not yet in the database +- Only new files are added; existing entries are not modified +- Large libraries may take several minutes to refresh depending on the number of files +- The command will report the number of files scanned and new entries added diff --git a/src/tagstudio/core/cli_driver.py b/src/tagstudio/core/cli_driver.py new file mode 100644 index 000000000..51babf486 --- /dev/null +++ b/src/tagstudio/core/cli_driver.py @@ -0,0 +1,99 @@ +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +"""Command-line interface driver for TagStudio.""" + +from pathlib import Path + +import structlog + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.refresh import RefreshTracker + +logger = structlog.get_logger(__name__) + + +class CliDriver: + """Handles command-line operations without launching the GUI.""" + + def __init__(self): + self.lib = Library() + + def refresh_library(self, library_path: str) -> int: + """Refresh a library to scan for new files. + + Args: + library_path: Path to the TagStudio library folder. + + Returns: + Exit code: 0 for success, 1 for failure. + """ + path = Path(library_path).expanduser() + + if not path.exists(): + logger.error("Library path does not exist", path=path) + return 1 + + logger.info("Opening library", path=path) + open_status = self.lib.open_library(path) + + if not open_status.success: + logger.error( + "Failed to open library", + message=open_status.message, + description=open_status.msg_description, + ) + return 1 + + if open_status.json_migration_req: + logger.error( + "Library requires JSON to SQLite migration. " + "Please open the library in the GUI to complete the migration." + ) + return 1 + + logger.info("Library opened successfully", path=path) + + # Perform the refresh + logger.info("Starting library refresh") + tracker = RefreshTracker(self.lib) + + try: + files_scanned = 0 + new_files_count = 0 + + # Refresh the library directory + for count in tracker.refresh_dir(path): + files_scanned = count + + new_files_count = tracker.files_count + + # Save newly found files + for _ in tracker.save_new_files(): + pass + + logger.info( + "Library refresh completed", + files_scanned=files_scanned, + new_files_added=new_files_count, + message=( + f"Refresh complete: scanned {files_scanned} files, " + f"added {new_files_count} new entries" + ), + ) + return 0 + + except (OSError, ValueError, RuntimeError) as e: + logger.error( + "Expected error during library refresh", + error_type=type(e).__name__, + error=str(e), + ) + return 1 + except Exception: + logger.exception("Unexpected error during library refresh") + return 1 + + finally: + self.lib.close() diff --git a/src/tagstudio/main.py b/src/tagstudio/main.py index 70411a27c..485309d3c 100755 --- a/src/tagstudio/main.py +++ b/src/tagstudio/main.py @@ -7,10 +7,12 @@ """TagStudio launcher.""" import argparse +import sys import traceback import structlog +from tagstudio.core.cli_driver import CliDriver from tagstudio.core.constants import VERSION, VERSION_BRANCH from tagstudio.qt.ts_qt import QtDriver @@ -44,6 +46,13 @@ def main(): type=str, help="Path to a TagStudio .ini or .plist cache file to use.", ) + parser.add_argument( + "-r", + "--refresh", + dest="refresh", + type=str, + help="Refresh a library without opening the GUI. Specify the library path.", + ) # parser.add_argument('--browse', dest='browse', action='store_true', # help='Jumps to entry browsing on startup.') @@ -64,6 +73,12 @@ def main(): ) args = parser.parse_args() + # Handle CLI-only operations + if args.refresh: + cli_driver = CliDriver() + exit_code = cli_driver.refresh_library(args.refresh) + sys.exit(exit_code) + driver = QtDriver(args) ui_name = "Qt" diff --git a/tests/test_cli_refresh.py b/tests/test_cli_refresh.py new file mode 100644 index 000000000..00217e18a --- /dev/null +++ b/tests/test_cli_refresh.py @@ -0,0 +1,37 @@ +# Copyright (C) 2025 +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +"""Tests for CLI refresh functionality.""" + +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + +CWD = Path(__file__).parent +sys.path.insert(0, str(CWD.parent)) + +from tagstudio.core.cli_driver import CliDriver + + +def test_cli_driver_refresh_nonexistent_library(): + """Test that refresh fails gracefully with a nonexistent library path.""" + driver = CliDriver() + result = driver.refresh_library("/nonexistent/path/that/does/not/exist") + assert result == 1, "Should return exit code 1 for nonexistent library" + + +def test_cli_driver_refresh_invalid_library(): + """Test that refresh successfully creates and refreshes a new library in empty dir.""" + with TemporaryDirectory() as tmpdir: + driver = CliDriver() + result = driver.refresh_library(tmpdir) + # Should succeed - creates new library if needed + assert result == 0, "Should return exit code 0 for newly created library" + + +def test_cli_driver_init(): + """Test that CliDriver initializes correctly.""" + driver = CliDriver() + assert driver.lib is not None, "CLI driver should have a Library instance" + assert hasattr(driver, "refresh_library"), "CLI driver should have refresh_library method"