Files
gnommo/gnommo/cache.py
T
2026-07-05 12:16:35 +02:00

258 lines
7.6 KiB
Python

"""GnommoCache - External storage extension for large media files.
Provides transparent fallback to external storage when files are not found locally.
Configure via ~/.gnommo.conf:
[cache]
path = /Volumes/GnommoDisk/gnommo
Files are looked up first locally, then in the cache at:
{cache_path}/{project_name}/{relative_path}
"""
import configparser
import os
from pathlib import Path
from typing import Optional, Tuple
_cache_config: Optional[dict] = None
_assets_config: Optional[dict] = None
_perf_config: Optional[dict] = None
def get_ffmpeg_thread_count() -> int:
"""Return FFmpeg thread count based on [performance] cpu_limit in ~/.gnommo.conf.
cpu_limit is a fraction of logical CPUs (e.g. 0.8 = 80%).
Defaults to 1 when not configured, which is safe on memory-constrained machines.
Example ~/.gnommo.conf:
[performance]
cpu_limit = 0.8
"""
global _perf_config
if _perf_config is None:
config_path = Path.home() / ".gnommo.conf"
_perf_config = {}
if config_path.exists():
cfg = configparser.ConfigParser()
cfg.read(config_path)
if cfg.has_option("performance", "cpu_limit"):
try:
_perf_config["cpu_limit"] = float(
cfg.get("performance", "cpu_limit")
)
except ValueError:
pass
cpu_limit = _perf_config.get("cpu_limit")
if cpu_limit is None:
return 1
cpu_count = os.cpu_count() or 1
return max(1, int(cpu_count * cpu_limit))
def get_render_chunk_size() -> Optional[int]:
"""Return slides-per-chunk for auto-chunked rendering, or None if not configured.
When set, cmd_render splits the filter graph into chunks of this many slides
to avoid OOM from allocating filter buffers for the entire video at once.
Example ~/.gnommo.conf:
[performance]
render_chunk_slides = 15
"""
global _perf_config
if _perf_config is None:
get_ffmpeg_thread_count() # populates _perf_config
val = _perf_config.get("render_chunk_slides")
if val is None:
return None
try:
return max(1, int(val))
except (ValueError, TypeError):
return None
def load_cache_config() -> Optional[Path]:
"""Load gnommo.conf and return cache path if configured.
Configuration file location: ~/.gnommo.conf
Returns:
Path to the cache root directory, or None if not configured.
"""
global _cache_config
if _cache_config is not None:
return _cache_config.get("path")
config_path = Path.home() / ".gnommo.conf"
if not config_path.exists():
_cache_config = {}
return None
config = configparser.ConfigParser()
config.read(config_path)
if config.has_option("cache", "path"):
cache_path = Path(config.get("cache", "path"))
_cache_config = {"path": cache_path}
return cache_path
_cache_config = {}
return None
def load_assets_process_cache() -> Optional[Path]:
"""Return the process-cache path on the [assets] disk, or None if not configured.
Derived by replacing the last component of the [assets] path with
that name + "cache". E.g.:
[assets] path = /Volumes/LaCie Jens/Projects/gnommo
→ process cache = /Volumes/LaCie Jens/Projects/gnommocache
This mirrors the GnommoDisk convention where the asset root is
/Volumes/GnommoDisk/gnommo and the process cache is /Volumes/GnommoDisk/gnommocache.
"""
assets_path = load_assets_config()
if assets_path is None:
return None
return assets_path.parent / (assets_path.name + "cache")
def load_assets_config() -> Optional[Path]:
"""Load gnommo.conf and return the [assets] path if configured.
The assets path is a second external fallback (e.g. a LaCie drive) with
the same directory layout as the gnommo project root. Resolution order is:
local → cache ([cache] path) → assets ([assets] path).
Example ~/.gnommo.conf:
[assets]
path = /Volumes/LaCie Jens/Projects/gnommo
"""
global _assets_config
if _assets_config is not None:
return _assets_config.get("path")
config_path = Path.home() / ".gnommo.conf"
if not config_path.exists():
_assets_config = {}
return None
config = configparser.ConfigParser()
config.read(config_path)
if config.has_option("assets", "path"):
assets_path = Path(config.get("assets", "path"))
_assets_config = {"path": assets_path}
return assets_path
_assets_config = {}
return None
def _resolve_against_base(
local_path: Path, project_path: Path, base: Path
) -> Optional[Path]:
"""Try to find local_path mirrored under base.
Tries two mappings:
1. project-relative: base / project_name / relative_to_project
2. gnommo-root-relative: base / relative_to_project_parent (e.g. shared_assets/…)
"""
try:
relative = local_path.relative_to(project_path)
p = base / project_path.name / relative
if p.exists():
return p
except ValueError:
pass
try:
relative = local_path.relative_to(project_path.parent)
p = base / relative
if p.exists():
return p
except ValueError:
pass
return None
def resolve_with_cache(
local_path: Path,
project_path: Path,
) -> Tuple[Path, bool]:
"""Resolve a file path with external-disk fallback (read-only).
Resolution order:
1. local_path (always checked first)
2. [cache] path — typically GnommoDisk
3. [assets] path — optional second drive (e.g. LaCie)
Returns:
Tuple of (resolved_path, is_from_external) where is_from_external=True
when the file was found on an external drive rather than locally.
"""
if local_path.exists():
return local_path, False
for base in (load_cache_config(), load_assets_config()):
if base is None:
continue
resolved = _resolve_against_base(local_path, project_path, base)
if resolved is not None:
return resolved, True
return local_path, False
def load_server_config() -> Optional[dict]:
"""Load server rsync config from ~/.gnommo.conf.
Expected config:
[server]
host = 76.13.144.52
user = root
path = /gnommo/project
Returns:
Dict with keys host, user, path (and optionally port), or None.
"""
config_path = Path.home() / ".gnommo.conf"
if not config_path.exists():
return None
config = configparser.ConfigParser()
config.read(config_path)
if not config.has_section("server"):
return None
host = config.get("server", "host", fallback=None)
user = config.get("server", "user", fallback="root")
path = config.get("server", "path", fallback="/gnommo/project")
port = config.get("server", "port", fallback="22")
if not host:
return None
return {"host": host, "user": user, "path": path, "port": port}
def is_cache_configured() -> bool:
"""Check if any external fallback is configured."""
return load_cache_config() is not None or load_assets_config() is not None
def get_cache_info() -> Optional[str]:
"""Get a human-readable string of all configured external paths."""
parts = []
for label, path in (("cache", load_cache_config()), ("assets", load_assets_config())):
if path is None:
continue
status = "connected" if path.exists() else "not connected"
parts.append(f"{path} ({status})")
return "; ".join(parts) if parts else None