riplex 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- riplex/__init__.py +3 -0
- riplex/cache.py +111 -0
- riplex/cli.py +2635 -0
- riplex/config.py +121 -0
- riplex/dedup.py +504 -0
- riplex/detect.py +150 -0
- riplex/disc_analysis.py +331 -0
- riplex/disc_provider.py +211 -0
- riplex/formatter.py +115 -0
- riplex/makemkv.py +573 -0
- riplex/matcher.py +445 -0
- riplex/metadata_provider.py +87 -0
- riplex/metadata_sources/__init__.py +0 -0
- riplex/metadata_sources/tmdb.py +208 -0
- riplex/models.py +137 -0
- riplex/normalize.py +147 -0
- riplex/organizer.py +674 -0
- riplex/planner.py +178 -0
- riplex/scanner.py +189 -0
- riplex/snapshot.py +144 -0
- riplex/splitter.py +126 -0
- riplex/tagger.py +146 -0
- riplex/ui.py +217 -0
- riplex-0.1.0.dist-info/METADATA +106 -0
- riplex-0.1.0.dist-info/RECORD +28 -0
- riplex-0.1.0.dist-info/WHEEL +5 -0
- riplex-0.1.0.dist-info/entry_points.txt +2 -0
- riplex-0.1.0.dist-info/top_level.txt +1 -0
riplex/__init__.py
ADDED
riplex/cache.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""File-based JSON cache for external API responses.
|
|
2
|
+
|
|
3
|
+
Each cached item is stored as a JSON file containing the payload and a
|
|
4
|
+
``fetched_at`` ISO timestamp. Items older than the configured TTL are
|
|
5
|
+
treated as missing.
|
|
6
|
+
|
|
7
|
+
Cache location follows OS conventions via platformdirs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import shutil
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from platformdirs import user_cache_dir
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_APP_NAME = "riplex"
|
|
24
|
+
|
|
25
|
+
# Module-level flag: when True, all reads return None (misses).
|
|
26
|
+
_disabled = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_cache_dir() -> Path:
|
|
30
|
+
"""Return the root cache directory, creating it if needed."""
|
|
31
|
+
p = Path(user_cache_dir(_APP_NAME))
|
|
32
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
return p
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def disable() -> None:
|
|
37
|
+
"""Disable the cache globally (``--no-cache``)."""
|
|
38
|
+
global _disabled
|
|
39
|
+
_disabled = True
|
|
40
|
+
log.debug("Cache disabled")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_disabled() -> bool:
|
|
44
|
+
"""Return whether caching is currently disabled."""
|
|
45
|
+
return _disabled
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _key_path(namespace: str, key: str) -> Path:
|
|
49
|
+
"""Build the filesystem path for a cache entry."""
|
|
50
|
+
return get_cache_dir() / namespace / f"{key}.json"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def hash_key(value: str) -> str:
|
|
54
|
+
"""Produce a filesystem-safe hash for arbitrary string keys."""
|
|
55
|
+
return hashlib.sha256(value.encode()).hexdigest()[:16]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cache_get(namespace: str, key: str, ttl_days: int = 30) -> dict | list | None:
|
|
59
|
+
"""Read a cached value, returning ``None`` on miss or expiry."""
|
|
60
|
+
if _disabled:
|
|
61
|
+
return None
|
|
62
|
+
path = _key_path(namespace, key)
|
|
63
|
+
if not path.exists():
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
67
|
+
fetched = datetime.fromisoformat(raw["fetched_at"])
|
|
68
|
+
age_days = (datetime.now(timezone.utc) - fetched).total_seconds() / 86400
|
|
69
|
+
if age_days > ttl_days:
|
|
70
|
+
log.debug("Cache expired: %s/%s (%.1f days old)", namespace, key, age_days)
|
|
71
|
+
path.unlink(missing_ok=True)
|
|
72
|
+
return None
|
|
73
|
+
log.debug("Cache hit: %s/%s (%.1f days old)", namespace, key, age_days)
|
|
74
|
+
return raw["data"]
|
|
75
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
|
76
|
+
log.debug("Cache corrupt, removing: %s/%s", namespace, key)
|
|
77
|
+
path.unlink(missing_ok=True)
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cache_set(namespace: str, key: str, data: dict | list) -> None:
|
|
82
|
+
"""Write a value to the cache."""
|
|
83
|
+
if _disabled:
|
|
84
|
+
return
|
|
85
|
+
path = _key_path(namespace, key)
|
|
86
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
payload = {
|
|
88
|
+
"fetched_at": datetime.now(timezone.utc).isoformat(),
|
|
89
|
+
"data": data,
|
|
90
|
+
}
|
|
91
|
+
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
|
92
|
+
log.debug("Cache write: %s/%s", namespace, key)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def clear(namespace: str | None = None) -> int:
|
|
96
|
+
"""Remove cached files. Returns the number of files removed."""
|
|
97
|
+
base = get_cache_dir()
|
|
98
|
+
target = base / namespace if namespace else base
|
|
99
|
+
if not target.exists():
|
|
100
|
+
return 0
|
|
101
|
+
count = sum(1 for _ in target.rglob("*.json"))
|
|
102
|
+
if namespace:
|
|
103
|
+
shutil.rmtree(target, ignore_errors=True)
|
|
104
|
+
else:
|
|
105
|
+
for child in base.iterdir():
|
|
106
|
+
if child.is_dir():
|
|
107
|
+
shutil.rmtree(child, ignore_errors=True)
|
|
108
|
+
elif child.suffix == ".json":
|
|
109
|
+
child.unlink(missing_ok=True)
|
|
110
|
+
log.debug("Cache cleared: %s (%d files)", target, count)
|
|
111
|
+
return count
|