258 lines
7.6 KiB
Python
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
|