Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions docs/cli-refresh.md
Original file line number Diff line number Diff line change
@@ -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
99 changes: 99 additions & 0 deletions src/tagstudio/core/cli_driver.py
Original file line number Diff line number Diff line change
@@ -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()
15 changes: 15 additions & 0 deletions src/tagstudio/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.')
Expand All @@ -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"

Expand Down
37 changes: 37 additions & 0 deletions tests/test_cli_refresh.py
Original file line number Diff line number Diff line change
@@ -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"