This commit is contained in:
2026-01-11 00:52:54 -08:00
parent 6eb02f22b5
commit 7c1959483f
6 changed files with 51 additions and 32 deletions

View File

@@ -21,7 +21,7 @@ from pathlib import Path, PurePosixPath
from threading import RLock
from typing import Optional, Dict, Any, List, Tuple, Set
from SYS.utils import sha256_file
from SYS.utils import sha256_file, expand_path
from SYS.logger import debug as mm_debug
logger = logging.getLogger(__name__)
@@ -208,7 +208,7 @@ class API_folder_store:
Args:
library_root: Path to the local library root directory
"""
self.library_root = Path(library_root)
self.library_root = expand_path(library_root)
self.db_path = self.library_root / self.DB_NAME
self.connection: Optional[sqlite3.Connection] = None
# sqlite3 connections are not safe for concurrent use across threads.
@@ -218,8 +218,13 @@ class API_folder_store:
self._init_db()
def _normalize_input_path(self, file_path: Path) -> Path:
p = Path(file_path).expanduser()
p = expand_path(file_path)
if not p.is_absolute():
# Check if it already seems to start with library_root but just wasn't absolute
# (e.g. library_root is "C:\foo" and p is "foo\bar" which might happen in some cases)
# though usually it's better to just join.
# But the recursive case happened because library_root was "$home/files" (not absolute)
# and p was "$home/files/..." (not absolute).
p = self.library_root / p
return p

View File

@@ -6,6 +6,7 @@ import re
from pathlib import Path
from typing import Any, Dict, Optional
from SYS.logger import log
from SYS.utils import expand_path
DEFAULT_CONFIG_FILENAME = "config.conf"
SCRIPT_DIR = Path(__file__).resolve().parent
@@ -13,6 +14,11 @@ SCRIPT_DIR = Path(__file__).resolve().parent
_CONFIG_CACHE: Dict[str, Dict[str, Any]] = {}
def clear_config_cache() -> None:
"""Clear the configuration cache."""
_CONFIG_CACHE.clear()
def _strip_inline_comment(line: str) -> str:
# Strip comments in a way that's friendly to common .conf usage:
# - Full-line comments starting with '#' or ';'
@@ -438,7 +444,7 @@ def resolve_output_dir(config: Dict[str, Any]) -> Path:
temp_value = config.get("temp")
if temp_value:
try:
path = Path(str(temp_value)).expanduser()
path = expand_path(temp_value)
# Verify we can access it (not a system directory with permission issues)
if path.exists() or path.parent.exists():
return path
@@ -449,7 +455,7 @@ def resolve_output_dir(config: Dict[str, Any]) -> Path:
outfile_value = config.get("outfile")
if outfile_value:
try:
return Path(str(outfile_value)).expanduser()
return expand_path(outfile_value)
except Exception:
pass
@@ -480,7 +486,7 @@ def get_local_storage_path(config: Dict[str, Any]) -> Optional[Path]:
if isinstance(default_config, dict):
path_str = default_config.get("path")
if path_str:
return Path(str(path_str)).expanduser()
return expand_path(path_str)
# Fall back to storage.local.path format
storage = config.get("storage", {})
@@ -489,14 +495,14 @@ def get_local_storage_path(config: Dict[str, Any]) -> Optional[Path]:
if isinstance(local_config, dict):
path_str = local_config.get("path")
if path_str:
return Path(str(path_str)).expanduser()
return expand_path(path_str)
# Fall back to old Local format
local_config = config.get("Local", {})
if isinstance(local_config, dict):
path_str = local_config.get("path")
if path_str:
return Path(str(path_str)).expanduser()
return expand_path(path_str)
return None
@@ -606,9 +612,9 @@ def resolve_cookies_path(
for value in values:
if not value:
continue
candidate = Path(str(value)).expanduser()
candidate = expand_path(value)
if not candidate.is_absolute():
candidate = (base_dir / candidate).expanduser()
candidate = expand_path(base_dir / candidate)
if candidate.is_file():
return candidate
@@ -622,7 +628,7 @@ def resolve_debug_log(config: Dict[str, Any]) -> Optional[Path]:
value = config.get("download_debug_log")
if not value:
return None
path = Path(str(value)).expanduser()
path = expand_path(value)
if not path.is_absolute():
path = Path.cwd() / path
return path

View File

@@ -11,11 +11,12 @@ try:
import ffmpeg # type: ignore
except Exception:
ffmpeg = None # type: ignore
import os
import base64
import logging
import time
from pathlib import Path
from typing import Any, Iterable
from typing import Any, Iterable, Optional
from datetime import datetime
from dataclasses import dataclass, field
from fnmatch import fnmatch
@@ -32,6 +33,14 @@ CHUNK_SIZE = 1024 * 1024 # 1 MiB
_format_logger = logging.getLogger(__name__)
def expand_path(p: str | Path | None) -> Path:
"""Expand ~ and environment variables in path."""
if p is None:
return None # type: ignore
expanded = os.path.expandvars(str(p))
return Path(expanded).expanduser()
def ensure_directory(path: Path) -> None:
"""Ensure *path* exists as a directory."""
try:

View File

@@ -9,7 +9,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from SYS.logger import debug, log
from SYS.utils import sha256_file
from SYS.utils import sha256_file, expand_path
from Store._base import Store
@@ -73,9 +73,8 @@ class Folder(Store):
try:
from API.folder import API_folder_store
from API.folder import LocalLibraryInitializer
from pathlib import Path
location_path = Path(self._location).expanduser()
location_path = expand_path(self._location)
# Use context manager to ensure connection is properly closed
with API_folder_store(location_path) as db:
@@ -124,9 +123,7 @@ class Folder(Store):
if not location:
return
from pathlib import Path
location_path = Path(location).expanduser()
location_path = expand_path(location)
location_str = str(location_path)
# Only migrate once per location
@@ -673,7 +670,7 @@ class Folder(Store):
match_all = query == "*" or (not query and bool(ext_filter))
results = []
search_dir = Path(self._location).expanduser()
search_dir = expand_path(self._location)
def _url_like_pattern(value: str) -> str:
# Interpret user patterns as substring matches (with optional glob wildcards).
@@ -1335,10 +1332,10 @@ class Folder(Store):
of the file path to find a directory with medios-macina.db."""
candidates: list[Path] = []
if self._location:
candidates.append(Path(self._location).expanduser())
candidates.append(expand_path(self._location))
cfg_root = get_local_storage_path(config) if config else None
if cfg_root:
candidates.append(Path(cfg_root).expanduser())
candidates.append(expand_path(cfg_root))
for root in candidates:
db_path = root / "medios-macina.db"
@@ -1369,7 +1366,7 @@ class Folder(Store):
if not normalized_hash:
return None
search_dir = Path(self._location).expanduser()
search_dir = expand_path(self._location)
from API.folder import API_folder_store
with API_folder_store(search_dir) as db:
@@ -1400,7 +1397,7 @@ class Folder(Store):
if not normalized_hash:
return None
search_dir = Path(self._location).expanduser()
search_dir = expand_path(self._location)
from API.folder import DatabaseAPI
with DatabaseAPI(search_dir) as api:
@@ -1460,7 +1457,7 @@ class Folder(Store):
from API.folder import API_folder_store
with API_folder_store(Path(self._location).expanduser()) as db:
with API_folder_store(expand_path(self._location)) as db:
db.set_relationship_by_hash(
alt_norm,
king_norm,
@@ -2150,7 +2147,7 @@ class Folder(Store):
if not raw:
return False
store_root = Path(self._location).expanduser()
store_root = expand_path(self._location)
# Support deletion by hash (common for store items where `path` is the hash).
file_hash = _normalize_hash(raw)
@@ -2159,7 +2156,7 @@ class Folder(Store):
if file_hash:
resolved_path = db.search_hash(file_hash)
else:
p = Path(raw)
p = expand_path(raw)
resolved_path = p if p.is_absolute() else (store_root / p)
if resolved_path is None:

View File

@@ -19,6 +19,7 @@ from pathlib import Path
from typing import Any, Dict, Iterable, Optional, Type
from SYS.logger import debug
from SYS.utils import expand_path
from Store._base import Store as BaseStore
@@ -169,8 +170,8 @@ class Store:
if not path_value:
return
temp_path = Path(str(temp_value)).expanduser().resolve()
backend_path = Path(str(path_value)).expanduser().resolve()
temp_path = expand_path(temp_value).resolve()
backend_path = expand_path(path_value).resolve()
if backend_path != temp_path:
return
@@ -230,7 +231,7 @@ class Store:
for key in list(kwargs.keys()):
if _normalize_config_key(key) in {"PATH",
"LOCATION"}:
kwargs[key] = str(Path(str(kwargs[key])).expanduser())
kwargs[key] = str(expand_path(kwargs[key]))
backend = store_cls(**kwargs)
@@ -411,7 +412,7 @@ def list_configured_backend_names(config: Optional[Dict[str, Any]]) -> list[str]
try:
temp_value = (config or {}).get("temp")
if temp_value:
temp_path = str(Path(str(temp_value)).expanduser().resolve())
temp_path = str(expand_path(temp_value).resolve())
for raw_store_type, instances in store_cfg.items():
if not isinstance(instances, dict):
continue
@@ -423,7 +424,7 @@ def list_configured_backend_names(config: Optional[Dict[str, Any]]) -> list[str]
path_value = instance_config.get("PATH") or instance_config.get("path")
if not path_value:
continue
if str(Path(str(path_value)).expanduser().resolve()) == temp_path:
if str(expand_path(path_value).resolve()) == temp_path:
if "temp" not in names:
names.append("temp")
except Exception:

3
TUI.py
View File

@@ -613,9 +613,10 @@ class PipelineHubApp(App):
def on_config_closed(self, result: Any = None) -> None:
"""Call when the config modal is dismissed to reload session data."""
try:
from SYS.config import load_config
from SYS.config import load_config, clear_config_cache
from cmdlet._shared import SharedArgs
# Force a fresh load from disk
clear_config_cache()
cfg = load_config()
# Clear UI state to show a "fresh" start