updated plugin refactor and added FTP and SCP plugins , also hydrusnetwork plugin migration
This commit is contained in:
+144
-39
@@ -8,6 +8,7 @@ import time
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from copy import deepcopy
|
||||
@@ -532,7 +533,7 @@ def resolve_cookies_path(
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
|
||||
plugin_cookie = resolve_plugin_asset_path("ytdlp", "cookies.txt", script_dir=base_dir)
|
||||
plugin_cookie = _resolve_ytdlp_plugin_cookie_path(base_dir)
|
||||
if plugin_cookie is not None:
|
||||
return plugin_cookie
|
||||
|
||||
@@ -542,6 +543,30 @@ def resolve_cookies_path(
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_ytdlp_plugin_cookie_path(base_dir: Path) -> Optional[Path]:
|
||||
plugin_cookie = resolve_plugin_asset_path("ytdlp", "cookies.txt", script_dir=base_dir)
|
||||
if plugin_cookie is not None:
|
||||
return plugin_cookie
|
||||
|
||||
plugin_dir = _resolve_app_root(base_dir) / "plugins" / "ytdlp"
|
||||
if not plugin_dir.is_dir():
|
||||
return None
|
||||
|
||||
plugin_cookie = plugin_dir / "cookies.txt"
|
||||
legacy_cookie = _resolve_app_root(base_dir) / "cookies.txt"
|
||||
try:
|
||||
if legacy_cookie.is_file() and not plugin_cookie.exists():
|
||||
plugin_cookie.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(legacy_cookie, plugin_cookie)
|
||||
return plugin_cookie
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if plugin_cookie.is_file():
|
||||
return plugin_cookie
|
||||
return None
|
||||
|
||||
|
||||
def resolve_debug_log(config: Dict[str, Any]) -> Optional[Path]:
|
||||
value = config.get("download_debug_log")
|
||||
if not value:
|
||||
@@ -721,6 +746,105 @@ def _count_changed_entries(old_config: Dict[str, Any], new_config: Dict[str, Any
|
||||
return len(changed) + len(removed)
|
||||
|
||||
|
||||
def _changed_entry_keys(old_config: Dict[str, Any], new_config: Dict[str, Any]) -> set[Tuple[str, str, str, str]]:
|
||||
old_entries = _flatten_config_entries(old_config or {})
|
||||
new_entries = _flatten_config_entries(new_config or {})
|
||||
keys = set(old_entries) | set(new_entries)
|
||||
return {key for key in keys if old_entries.get(key, _CONFIG_MISSING) != new_entries.get(key, _CONFIG_MISSING)}
|
||||
|
||||
|
||||
def _config_from_flattened_entries(
|
||||
entries: Dict[Tuple[str, str, str, str], Any],
|
||||
) -> Dict[str, Any]:
|
||||
config: Dict[str, Any] = {}
|
||||
for (category, subtype, item_name, key), value in entries.items():
|
||||
if category == "global":
|
||||
config[key] = value
|
||||
continue
|
||||
|
||||
if category == "store":
|
||||
store_block = config.setdefault("store", {})
|
||||
subtype_block = store_block.setdefault(subtype, {})
|
||||
item_block = subtype_block.setdefault(item_name, {})
|
||||
item_block[key] = value
|
||||
continue
|
||||
|
||||
if category in {"provider", "tool"}:
|
||||
category_block = config.setdefault(category, {})
|
||||
subtype_block = category_block.setdefault(subtype, {})
|
||||
subtype_block[key] = value
|
||||
continue
|
||||
|
||||
category_block = config.setdefault(category, {})
|
||||
if isinstance(category_block, dict):
|
||||
subtype_block = category_block.setdefault(subtype, {})
|
||||
if isinstance(subtype_block, dict):
|
||||
item_block = subtype_block.setdefault(item_name, {})
|
||||
if isinstance(item_block, dict):
|
||||
item_block[key] = value
|
||||
|
||||
_normalize_plugin_config_aliases(config)
|
||||
_sync_alldebrid_api_key(config)
|
||||
return config
|
||||
|
||||
|
||||
def _merge_non_conflicting_config_changes(
|
||||
base_config: Dict[str, Any],
|
||||
disk_config: Dict[str, Any],
|
||||
local_config: Dict[str, Any],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
local_changed = _changed_entry_keys(base_config, local_config)
|
||||
if not local_changed:
|
||||
return deepcopy(disk_config)
|
||||
|
||||
disk_changed = _changed_entry_keys(base_config, disk_config)
|
||||
if local_changed & disk_changed:
|
||||
return None
|
||||
|
||||
merged_entries = dict(_flatten_config_entries(disk_config or {}))
|
||||
local_entries = _flatten_config_entries(local_config or {})
|
||||
for key in local_changed:
|
||||
if key in local_entries:
|
||||
merged_entries[key] = local_entries[key]
|
||||
else:
|
||||
merged_entries.pop(key, None)
|
||||
return _config_from_flattened_entries(merged_entries)
|
||||
|
||||
|
||||
def _extract_expected_alldebrid_key(config: Dict[str, Any]) -> Optional[str]:
|
||||
expected_key = None
|
||||
try:
|
||||
providers = config.get("provider", {}) if isinstance(config, dict) else {}
|
||||
if isinstance(providers, dict):
|
||||
entry = providers.get("alldebrid")
|
||||
if entry is not None:
|
||||
if isinstance(entry, dict):
|
||||
for k in ("api_key", "API_KEY", "apikey", "APIKEY"):
|
||||
v = entry.get(k)
|
||||
if isinstance(v, str) and v.strip():
|
||||
expected_key = v.strip()
|
||||
break
|
||||
elif isinstance(entry, str) and entry.strip():
|
||||
expected_key = entry.strip()
|
||||
if not expected_key:
|
||||
store_block = config.get("store", {}) if isinstance(config, dict) else {}
|
||||
debrid = store_block.get("debrid") if isinstance(store_block, dict) else None
|
||||
if isinstance(debrid, dict):
|
||||
srv = debrid.get("all-debrid")
|
||||
if isinstance(srv, dict):
|
||||
for k in ("api_key", "API_KEY", "apikey", "APIKEY"):
|
||||
v = srv.get(k)
|
||||
if isinstance(v, str) and v.strip():
|
||||
expected_key = v.strip()
|
||||
break
|
||||
elif isinstance(srv, str) and srv.strip():
|
||||
expected_key = srv.strip()
|
||||
except Exception as exc:
|
||||
logger.debug("Failed to determine expected AllDebrid key: %s", exc, exc_info=True)
|
||||
expected_key = None
|
||||
return expected_key
|
||||
|
||||
|
||||
def load_config(*, emit_summary: bool = True) -> Dict[str, Any]:
|
||||
global _CONFIG_CACHE, _LAST_SAVED_CONFIG, _CONFIG_SUMMARY_PENDING
|
||||
if _CONFIG_CACHE:
|
||||
@@ -838,6 +962,7 @@ def save_config(config: Dict[str, Any]) -> int:
|
||||
def _write_entries() -> int:
|
||||
global _CONFIG_CACHE, _LAST_SAVED_CONFIG
|
||||
count = 0
|
||||
config_to_write = config
|
||||
# Use the transaction-provided connection directly to avoid re-acquiring
|
||||
# the connection lock via db.* helpers which can lead to deadlock.
|
||||
with db.transaction() as conn:
|
||||
@@ -861,14 +986,22 @@ def save_config(config: Dict[str, Any]) -> int:
|
||||
_CONFIG_CACHE = current_disk
|
||||
_LAST_SAVED_CONFIG = deepcopy(current_disk)
|
||||
return 0
|
||||
# Otherwise, abort to avoid overwriting external changes
|
||||
raise ConfigSaveConflict(
|
||||
"Configuration on disk changed since you started editing; save aborted to prevent overwrite. Reload and reapply your changes."
|
||||
merged_config = _merge_non_conflicting_config_changes(
|
||||
previous_config,
|
||||
current_disk,
|
||||
config,
|
||||
)
|
||||
if merged_config is None:
|
||||
# Otherwise, abort to avoid overwriting external changes
|
||||
raise ConfigSaveConflict(
|
||||
"Configuration on disk changed since you started editing; save aborted to prevent overwrite. Reload and reapply your changes."
|
||||
)
|
||||
config_to_write = merged_config
|
||||
log("Config save rebased local changes onto newer disk configuration.")
|
||||
|
||||
# Proceed with writing when no conflicting external changes detected
|
||||
conn.execute("DELETE FROM config")
|
||||
for key, value in config.items():
|
||||
for key, value in config_to_write.items():
|
||||
if key in ('store', 'provider', 'tool') and isinstance(value, dict):
|
||||
for subtype, instances in value.items():
|
||||
if not isinstance(instances, dict):
|
||||
@@ -904,6 +1037,8 @@ def save_config(config: Dict[str, Any]) -> int:
|
||||
("global", "none", "none", key, val_str),
|
||||
)
|
||||
count += 1
|
||||
_CONFIG_CACHE = config_to_write
|
||||
_LAST_SAVED_CONFIG = deepcopy(config_to_write)
|
||||
return count
|
||||
|
||||
|
||||
@@ -964,9 +1099,6 @@ def save_config(config: Dict[str, Any]) -> int:
|
||||
logger.exception("Failed to release save lock after CRITICAL configuration save failure: %s", exc)
|
||||
raise
|
||||
|
||||
clear_config_cache()
|
||||
_CONFIG_CACHE = config
|
||||
_LAST_SAVED_CONFIG = deepcopy(config)
|
||||
return saved_entries
|
||||
|
||||
|
||||
@@ -988,37 +1120,10 @@ def save_config_and_verify(config: Dict[str, Any], retries: int = 3, delay: floa
|
||||
AllDebrid) were written successfully. If verification fails after the
|
||||
configured number of retries, a RuntimeError is raised.
|
||||
"""
|
||||
# Detect an API key that should be verified (provider or store-backed)
|
||||
expected_key = None
|
||||
try:
|
||||
providers = config.get("provider", {}) if isinstance(config, dict) else {}
|
||||
if isinstance(providers, dict):
|
||||
entry = providers.get("alldebrid")
|
||||
if entry is not None:
|
||||
# _extract_api_key is a small internal helper; reuse the implementation here
|
||||
if isinstance(entry, dict):
|
||||
for k in ("api_key", "API_KEY", "apikey", "APIKEY"):
|
||||
v = entry.get(k)
|
||||
if isinstance(v, str) and v.strip():
|
||||
expected_key = v.strip()
|
||||
break
|
||||
elif isinstance(entry, str) and entry.strip():
|
||||
expected_key = entry.strip()
|
||||
if not expected_key:
|
||||
store_block = config.get("store", {}) if isinstance(config, dict) else {}
|
||||
debrid = store_block.get("debrid") if isinstance(store_block, dict) else None
|
||||
if isinstance(debrid, dict):
|
||||
srv = debrid.get("all-debrid")
|
||||
if isinstance(srv, dict):
|
||||
for k in ("api_key", "API_KEY", "apikey", "APIKEY"):
|
||||
v = srv.get(k)
|
||||
if isinstance(v, str) and v.strip():
|
||||
expected_key = v.strip()
|
||||
break
|
||||
elif isinstance(srv, str) and srv.strip():
|
||||
expected_key = srv.strip()
|
||||
except Exception as exc:
|
||||
logger.debug("Failed to determine expected key for save verification: %s", exc, exc_info=True)
|
||||
# Only perform the extra verification loop when the AllDebrid key actually changed.
|
||||
expected_key = _extract_expected_alldebrid_key(config)
|
||||
baseline_key = _extract_expected_alldebrid_key(_LAST_SAVED_CONFIG)
|
||||
if expected_key == baseline_key:
|
||||
expected_key = None
|
||||
|
||||
last_exc: Exception | None = None
|
||||
|
||||
Reference in New Issue
Block a user