Add YAPF style + ignore, and format tracked Python files

This commit is contained in:
2025-12-29 18:42:02 -08:00
parent c019c00aed
commit 507946a3e4
108 changed files with 11664 additions and 6494 deletions

View File

@@ -85,7 +85,8 @@ class HydrusNetwork:
raise ValueError("Hydrus base URL is required")
self.url = self.url.rstrip("/")
parsed = urlsplit(self.url)
if parsed.scheme not in {"http", "https"}:
if parsed.scheme not in {"http",
"https"}:
raise ValueError("Hydrus base URL must use http or https")
self.scheme = parsed.scheme
self.hostname = parsed.hostname or "localhost"
@@ -114,7 +115,8 @@ class HydrusNetwork:
return path
def _perform_request(self, spec: HydrusRequestSpec) -> Any:
headers: dict[str, str] = {}
headers: dict[str,
str] = {}
# Use session key if available, otherwise use access key
if self._session_key:
@@ -138,7 +140,9 @@ class HydrusNetwork:
content_type = ""
try:
with HTTPClient(timeout=self.timeout, headers=headers, verify_ssl=False) as client:
with HTTPClient(timeout=self.timeout,
headers=headers,
verify_ssl=False) as client:
response = None
if spec.file_path is not None:
@@ -149,7 +153,8 @@ class HydrusNetwork:
raise FileNotFoundError(error_msg)
file_size = file_path.stat().st_size
headers["Content-Type"] = spec.content_type or "application/octet-stream"
headers["Content-Type"
] = spec.content_type or "application/octet-stream"
# Do not set Content-Length when streaming an iterator body.
# If the file size changes between stat() and read() (or the source is truncated),
# h11 will raise: "Too little data for declared Content-Length".
@@ -239,7 +244,9 @@ class HydrusNetwork:
body = response.content
content_type = response.headers.get("Content-Type", "") or ""
logger.debug(f"{self._log_prefix()} Response {status} {reason} ({len(body)} bytes)")
logger.debug(
f"{self._log_prefix()} Response {status} {reason} ({len(body)} bytes)"
)
except (httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError) as exc:
msg = f"Hydrus unavailable: {exc}"
@@ -292,7 +299,10 @@ class HydrusNetwork:
# Retry the request with new session key
return self._perform_request(spec)
except Exception as retry_error:
logger.error(f"{self._log_prefix()} Retry failed: {retry_error}", exc_info=True)
logger.error(
f"{self._log_prefix()} Retry failed: {retry_error}",
exc_info=True
)
# If retry fails, raise the original error
raise HydrusRequestError(status, message, payload) from retry_error
@@ -311,7 +321,10 @@ class HydrusNetwork:
Raises HydrusRequestError if the request fails.
"""
if not self.access_key:
raise HydrusRequestError(401, "Cannot acquire session key: no access key configured")
raise HydrusRequestError(
401,
"Cannot acquire session key: no access key configured"
)
# Temporarily use access key to get session key
original_session_key = self._session_key
@@ -323,7 +336,9 @@ class HydrusNetwork:
if not session_key:
raise HydrusRequestError(
500, "Session key response missing 'session_key' field", result
500,
"Session key response missing 'session_key' field",
result
)
self._session_key = session_key
@@ -345,7 +360,12 @@ class HydrusNetwork:
return self._session_key
return self._acquire_session_key()
def _get(self, endpoint: str, *, query: dict[str, Any] | None = None) -> dict[str, Any]:
def _get(self,
endpoint: str,
*,
query: dict[str,
Any] | None = None) -> dict[str,
Any]:
spec = HydrusRequestSpec("GET", endpoint, query=query)
return cast(dict[str, Any], self._perform_request(spec))
@@ -353,12 +373,18 @@ class HydrusNetwork:
self,
endpoint: str,
*,
data: dict[str, Any] | None = None,
data: dict[str,
Any] | None = None,
file_path: Path | None = None,
content_type: str | None = None,
) -> dict[str, Any]:
) -> dict[str,
Any]:
spec = HydrusRequestSpec(
"POST", endpoint, data=data, file_path=file_path, content_type=content_type
"POST",
endpoint,
data=data,
file_path=file_path,
content_type=content_type
)
return cast(dict[str, Any], self._perform_request(spec))
@@ -397,12 +423,19 @@ class HydrusNetwork:
Required JSON args: {"hashes": [<sha256 hex>, ...]}
"""
hash_list = self._ensure_hashes(hashes)
body = {"hashes": hash_list}
body = {
"hashes": hash_list
}
return self._post("/add_files/undelete_files", data=body)
def delete_files(
self, hashes: Union[str, Iterable[str]], *, reason: str | None = None
) -> dict[str, Any]:
self,
hashes: Union[str,
Iterable[str]],
*,
reason: str | None = None
) -> dict[str,
Any]:
"""Delete files in Hydrus.
Hydrus Client API: POST /add_files/delete_files
@@ -410,98 +443,166 @@ class HydrusNetwork:
Optional JSON args: {"reason": "..."}
"""
hash_list = self._ensure_hashes(hashes)
body: dict[str, Any] = {"hashes": hash_list}
body: dict[str,
Any] = {
"hashes": hash_list
}
if isinstance(reason, str) and reason.strip():
body["reason"] = reason.strip()
return self._post("/add_files/delete_files", data=body)
def clear_file_deletion_record(self, hashes: Union[str, Iterable[str]]) -> dict[str, Any]:
def clear_file_deletion_record(self,
hashes: Union[str,
Iterable[str]]) -> dict[str,
Any]:
"""Clear Hydrus's file deletion record for the provided hashes.
Hydrus Client API: POST /add_files/clear_file_deletion_record
Required JSON args: {"hashes": [<sha256 hex>, ...]}
"""
hash_list = self._ensure_hashes(hashes)
body = {"hashes": hash_list}
body = {
"hashes": hash_list
}
return self._post("/add_files/clear_file_deletion_record", data=body)
def add_tag(
self, hash: Union[str, Iterable[str]], tags: Iterable[str], service_name: str
) -> dict[str, Any]:
self,
hash: Union[str,
Iterable[str]],
tags: Iterable[str],
service_name: str
) -> dict[str,
Any]:
hash = self._ensure_hashes(hash)
body = {"hashes": hash, "service_names_to_tags": {service_name: list(tags)}}
body = {
"hashes": hash,
"service_names_to_tags": {
service_name: list(tags)
}
}
return self._post("/add_tags/add_tags", data=body)
def delete_tag(
self,
file_hashes: Union[str, Iterable[str]],
file_hashes: Union[str,
Iterable[str]],
tags: Iterable[str],
service_name: str,
*,
action: int = 1,
) -> dict[str, Any]:
) -> dict[str,
Any]:
hashes = self._ensure_hashes(file_hashes)
body = {
"hashes": hashes,
"service_names_to_actions_to_tags": {service_name: {action: list(tags)}},
"service_names_to_actions_to_tags": {
service_name: {
action: list(tags)
}
},
}
return self._post("/add_tags/add_tags", data=body)
def add_tags_by_key(
self, hash: Union[str, Iterable[str]], tags: Iterable[str], service_key: str
) -> dict[str, Any]:
self,
hash: Union[str,
Iterable[str]],
tags: Iterable[str],
service_key: str
) -> dict[str,
Any]:
hash = self._ensure_hashes(hash)
body = {"hashes": hash, "service_keys_to_tags": {service_key: list(tags)}}
body = {
"hashes": hash,
"service_keys_to_tags": {
service_key: list(tags)
}
}
return self._post("/add_tags/add_tags", data=body)
def delete_tags_by_key(
self,
file_hashes: Union[str, Iterable[str]],
file_hashes: Union[str,
Iterable[str]],
tags: Iterable[str],
service_key: str,
*,
action: int = 1,
) -> dict[str, Any]:
) -> dict[str,
Any]:
hashes = self._ensure_hashes(file_hashes)
body = {
"hashes": hashes,
"service_keys_to_actions_to_tags": {service_key: {action: list(tags)}},
"service_keys_to_actions_to_tags": {
service_key: {
action: list(tags)
}
},
}
return self._post("/add_tags/add_tags", data=body)
def associate_url(self, file_hashes: Union[str, Iterable[str]], url: str) -> dict[str, Any]:
def associate_url(self,
file_hashes: Union[str,
Iterable[str]],
url: str) -> dict[str,
Any]:
hashes = self._ensure_hashes(file_hashes)
if len(hashes) == 1:
body = {"hash": hashes[0], "url_to_add": url}
body = {
"hash": hashes[0],
"url_to_add": url
}
return self._post("/add_urls/associate_url", data=body)
results: dict[str, Any] = {}
results: dict[str,
Any] = {}
for file_hash in hashes:
body = {"hash": file_hash, "url_to_add": url}
body = {
"hash": file_hash,
"url_to_add": url
}
results[file_hash] = self._post("/add_urls/associate_url", data=body)
return {"batched": results}
return {
"batched": results
}
def delete_url(self, file_hashes: Union[str, Iterable[str]], url: str) -> dict[str, Any]:
def delete_url(self,
file_hashes: Union[str,
Iterable[str]],
url: str) -> dict[str,
Any]:
hashes = self._ensure_hashes(file_hashes)
if len(hashes) == 1:
body = {"hash": hashes[0], "url_to_delete": url}
body = {
"hash": hashes[0],
"url_to_delete": url
}
return self._post("/add_urls/associate_url", data=body)
results: dict[str, Any] = {}
results: dict[str,
Any] = {}
for file_hash in hashes:
body = {"hash": file_hash, "url_to_delete": url}
body = {
"hash": file_hash,
"url_to_delete": url
}
results[file_hash] = self._post("/add_urls/associate_url", data=body)
return {"batched": results}
return {
"batched": results
}
def set_notes(
self,
file_hash: str,
notes: dict[str, str],
notes: dict[str,
str],
*,
merge_cleverly: bool = False,
extend_existing_note_if_possible: bool = True,
conflict_resolution: int = 3,
) -> dict[str, Any]:
) -> dict[str,
Any]:
"""Add or update notes associated with a file.
Hydrus Client API: POST /add_notes/set_notes
@@ -514,11 +615,17 @@ class HydrusNetwork:
if not file_hash:
raise ValueError("file_hash must not be empty")
body: dict[str, Any] = {"hash": file_hash, "notes": notes}
body: dict[str,
Any] = {
"hash": file_hash,
"notes": notes
}
if merge_cleverly:
body["merge_cleverly"] = True
body["extend_existing_note_if_possible"] = bool(extend_existing_note_if_possible)
body["extend_existing_note_if_possible"] = bool(
extend_existing_note_if_possible
)
body["conflict_resolution"] = int(conflict_resolution)
return self._post("/add_notes/set_notes", data=body)
@@ -526,7 +633,8 @@ class HydrusNetwork:
self,
file_hash: str,
note_names: Sequence[str],
) -> dict[str, Any]:
) -> dict[str,
Any]:
"""Delete notes associated with a file.
Hydrus Client API: POST /add_notes/delete_notes
@@ -540,20 +648,30 @@ class HydrusNetwork:
if not file_hash:
raise ValueError("file_hash must not be empty")
body = {"hash": file_hash, "note_names": names}
body = {
"hash": file_hash,
"note_names": names
}
return self._post("/add_notes/delete_notes", data=body)
def get_file_relationships(self, file_hash: str) -> dict[str, Any]:
query = {"hash": file_hash}
return self._get("/manage_file_relationships/get_file_relationships", query=query)
query = {
"hash": file_hash
}
return self._get(
"/manage_file_relationships/get_file_relationships",
query=query
)
def set_relationship(
self,
hash_a: str,
hash_b: str,
relationship: Union[str, int],
relationship: Union[str,
int],
do_default_content_merge: bool = False,
) -> dict[str, Any]:
) -> dict[str,
Any]:
"""Set a relationship between two files in Hydrus.
This wraps Hydrus Client API: POST /manage_file_relationships/set_file_relationships.
@@ -609,7 +727,10 @@ class HydrusNetwork:
# Hydrus does not accept 'king' as a relationship; this maps to 'A is better'.
"king": 4,
}
relationship = rel_map.get(relationship.lower().strip(), 3) # Default to alternates
relationship = rel_map.get(
relationship.lower().strip(),
3
) # Default to alternates
body = {
"relationships": [
@@ -621,7 +742,10 @@ class HydrusNetwork:
}
]
}
return self._post("/manage_file_relationships/set_file_relationships", data=body)
return self._post(
"/manage_file_relationships/set_file_relationships",
data=body
)
def get_services(self) -> dict[str, Any]:
return self._get("/get_services")
@@ -639,17 +763,24 @@ class HydrusNetwork:
file_sort_type: int | None = None,
file_sort_asc: bool | None = None,
file_sort_key: str | None = None,
) -> dict[str, Any]:
) -> dict[str,
Any]:
if not tags:
raise ValueError("tags must not be empty")
query: dict[str, Any] = {}
query: dict[str,
Any] = {}
query_fields = [
("tags", tags, lambda v: json.dumps(list(v))),
("file_service_name", file_service_name, lambda v: v),
("return_hashes", return_hashes, lambda v: "true" if v else None),
("return_file_ids", return_file_ids, lambda v: "true" if v else None),
("return_file_count", return_file_count, lambda v: "true" if v else None),
("tags",
tags, lambda v: json.dumps(list(v))),
("file_service_name",
file_service_name, lambda v: v),
("return_hashes",
return_hashes, lambda v: "true" if v else None),
("return_file_ids",
return_file_ids, lambda v: "true" if v else None),
("return_file_count",
return_file_count, lambda v: "true" if v else None),
(
"include_current_tags",
include_current_tags,
@@ -660,13 +791,17 @@ class HydrusNetwork:
include_pending_tags,
lambda v: "true" if v else "false" if v is not None else None,
),
("file_sort_type", file_sort_type, lambda v: str(v) if v is not None else None),
(
"file_sort_type",
file_sort_type, lambda v: str(v) if v is not None else None
),
(
"file_sort_asc",
file_sort_asc,
lambda v: "true" if v else "false" if v is not None else None,
),
("file_sort_key", file_sort_key, lambda v: v),
("file_sort_key",
file_sort_key, lambda v: v),
]
for key, value, formatter in query_fields:
@@ -689,24 +824,33 @@ class HydrusNetwork:
include_size: bool = True,
include_mime: bool = False,
include_notes: bool = False,
) -> dict[str, Any]:
) -> dict[str,
Any]:
if not file_ids and not hashes:
raise ValueError("Either file_ids or hashes must be provided")
query: dict[str, Any] = {}
query: dict[str,
Any] = {}
query_fields = [
("file_ids", file_ids, lambda v: json.dumps(list(v))),
("hashes", hashes, lambda v: json.dumps(list(v))),
("file_ids",
file_ids, lambda v: json.dumps(list(v))),
("hashes",
hashes, lambda v: json.dumps(list(v))),
(
"include_service_keys_to_tags",
include_service_keys_to_tags,
lambda v: "true" if v else None,
),
("include_file_url", include_file_url, lambda v: "true" if v else None),
("include_duration", include_duration, lambda v: "true" if v else None),
("include_size", include_size, lambda v: "true" if v else None),
("include_mime", include_mime, lambda v: "true" if v else None),
("include_notes", include_notes, lambda v: "true" if v else None),
("include_file_url",
include_file_url, lambda v: "true" if v else None),
("include_duration",
include_duration, lambda v: "true" if v else None),
("include_size",
include_size, lambda v: "true" if v else None),
("include_mime",
include_mime, lambda v: "true" if v else None),
("include_notes",
include_notes, lambda v: "true" if v else None),
]
for key, value, formatter in query_fields:
@@ -720,7 +864,9 @@ class HydrusNetwork:
def get_file_path(self, file_hash: str) -> dict[str, Any]:
"""Get the local file system path for a given file hash."""
query = {"hash": file_hash}
query = {
"hash": file_hash
}
return self._get("/get_files/file_path", query=query)
def file_url(self, file_hash: str) -> str:
@@ -752,7 +898,10 @@ class HydrusCliOptions:
debug: bool = False
@classmethod
def from_namespace(cls: Type[HydrusCliOptionsT], namespace: Any) -> HydrusCliOptionsT:
def from_namespace(
cls: Type[HydrusCliOptionsT],
namespace: Any
) -> HydrusCliOptionsT:
accept_header = namespace.accept or "application/cbor"
body_bytes: bytes | None = None
body_path: Path | None = None
@@ -785,7 +934,8 @@ def hydrus_request(args, parser) -> int:
if not parsed.hostname:
parser.error("Invalid Hydrus URL")
headers: dict[str, str] = {}
headers: dict[str,
str] = {}
if options.access_key:
headers["Hydrus-Client-API-Access-Key"] = options.access_key
if options.accept:
@@ -797,7 +947,10 @@ def hydrus_request(args, parser) -> int:
body_path = options.body_path
if not body_path.is_file():
parser.error(f"File not found: {body_path}")
headers.setdefault("Content-Type", options.content_type or "application/octet-stream")
headers.setdefault(
"Content-Type",
options.content_type or "application/octet-stream"
)
headers["Content-Length"] = str(body_path.stat().st_size)
elif options.body_bytes is not None:
request_body_bytes = options.body_bytes
@@ -820,13 +973,17 @@ def hydrus_request(args, parser) -> int:
port = 443 if parsed.scheme == "https" else 80
connection_cls = (
http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
http.client.HTTPSConnection
if parsed.scheme == "https" else http.client.HTTPConnection
)
host = parsed.hostname or "localhost"
connection = connection_cls(host, port, timeout=options.timeout)
if options.debug:
log(f"Hydrus connecting to {parsed.scheme}://{host}:{port}{path}", file=sys.stderr)
log(
f"Hydrus connecting to {parsed.scheme}://{host}:{port}{path}",
file=sys.stderr
)
response_bytes: bytes = b""
content_type = ""
status = 0
@@ -835,12 +992,17 @@ def hydrus_request(args, parser) -> int:
with body_path.open("rb") as handle:
if options.debug:
size_hint = headers.get("Content-Length", "unknown")
log(f"Hydrus sending file body ({size_hint} bytes)", file=sys.stderr)
log(
f"Hydrus sending file body ({size_hint} bytes)",
file=sys.stderr
)
connection.putrequest(options.method, path)
host_header = host
if (parsed.scheme == "http" and port not in (80, None)) or (
parsed.scheme == "https" and port not in (443, None)
):
if (parsed.scheme == "http"
and port not in (80,
None)) or (parsed.scheme == "https"
and port not in (443,
None)):
host_header = f"{host}:{port}"
connection.putheader("Host", host_header)
for key, value in headers.items():
@@ -853,20 +1015,34 @@ def hydrus_request(args, parser) -> int:
break
connection.send(chunk)
if options.debug:
log("[downlow.py] Hydrus upload complete; awaiting response", file=sys.stderr)
log(
"[downlow.py] Hydrus upload complete; awaiting response",
file=sys.stderr
)
else:
if options.debug:
size_hint = "none" if request_body_bytes is None else str(len(request_body_bytes))
size_hint = "none" if request_body_bytes is None else str(
len(request_body_bytes)
)
log(f"Hydrus sending request body bytes={size_hint}", file=sys.stderr)
sanitized_headers = {k: v for k, v in headers.items() if v}
sanitized_headers = {
k: v
for k, v in headers.items() if v
}
connection.request(
options.method, path, body=request_body_bytes, headers=sanitized_headers
options.method,
path,
body=request_body_bytes,
headers=sanitized_headers
)
response = connection.getresponse()
status = response.status
response_bytes = response.read()
if options.debug:
log(f"Hydrus response received ({len(response_bytes)} bytes)", file=sys.stderr)
log(
f"Hydrus response received ({len(response_bytes)} bytes)",
file=sys.stderr
)
content_type = response.getheader("Content-Type", "")
except (OSError, http.client.HTTPException) as exc:
log(f"HTTP error: {exc}", file=sys.stderr)
@@ -890,7 +1066,10 @@ def hydrus_request(args, parser) -> int:
except (json.JSONDecodeError, UnicodeDecodeError):
payload = response_bytes.decode("utf-8", "replace")
elif payload is None and expect_cbor and decode_error is not None:
log(f"Expected CBOR response but decoding failed: {decode_error}", file=sys.stderr)
log(
f"Expected CBOR response but decoding failed: {decode_error}",
file=sys.stderr
)
return 1
json_ready = jsonify(payload) if isinstance(payload, (dict, list)) else payload
if options.debug:
@@ -900,7 +1079,10 @@ def hydrus_request(args, parser) -> int:
elif json_ready is None:
log("{}")
else:
log(json.dumps({"value": json_ready}, ensure_ascii=False))
log(json.dumps({
"value": json_ready
},
ensure_ascii=False))
return 0 if 200 <= status < 400 else 1
@@ -1030,13 +1212,16 @@ def hydrus_export(args, _parser) -> int:
hydrus_url = getattr(args, "hydrus_url", None)
if not hydrus_url:
try:
from config import load_config, get_hydrus_url
from SYS.config import load_config, get_hydrus_url
hydrus_url = get_hydrus_url(load_config())
except Exception as exc:
hydrus_url = None
if os.environ.get("DOWNLOW_DEBUG"):
log(f"hydrus-export could not load Hydrus URL: {exc}", file=sys.stderr)
log(
f"hydrus-export could not load Hydrus URL: {exc}",
file=sys.stderr
)
if hydrus_url:
try:
setattr(args, "hydrus_url", hydrus_url)
@@ -1047,19 +1232,30 @@ def hydrus_export(args, _parser) -> int:
if hydrus_url and file_hash:
try:
client = HydrusNetwork(
url=hydrus_url, access_key=args.access_key, timeout=args.timeout
url=hydrus_url,
access_key=args.access_key,
timeout=args.timeout
)
meta_response = client.fetch_file_metadata(hashes=[file_hash], include_mime=True)
entries = meta_response.get("metadata") if isinstance(meta_response, dict) else None
meta_response = client.fetch_file_metadata(
hashes=[file_hash],
include_mime=True
)
entries = meta_response.get("metadata") if isinstance(
meta_response,
dict
) else None
if isinstance(entries, list) and entries:
entry = entries[0]
ext_value = _normalise_ext(
entry.get("ext") if isinstance(entry, dict) else None
entry.get("ext") if isinstance(entry,
dict) else None
)
if ext_value:
resolved_suffix = ext_value
else:
mime_value = entry.get("mime") if isinstance(entry, dict) else None
mime_value = entry.get("mime"
) if isinstance(entry,
dict) else None
resolved_suffix = _extension_from_mime(mime_value)
except Exception as exc: # pragma: no cover - defensive
if os.environ.get("DOWNLOW_DEBUG"):
@@ -1072,7 +1268,8 @@ def hydrus_export(args, _parser) -> int:
source_suffix = resolved_suffix
suffix = source_suffix or ".hydrus"
if suffix and output_path.suffix.lower() in {"", ".bin"}:
if suffix and output_path.suffix.lower() in {"",
".bin"}:
if output_path.suffix.lower() != suffix.lower():
output_path = output_path.with_suffix(suffix)
target_dir = output_path.parent
@@ -1082,7 +1279,11 @@ def hydrus_export(args, _parser) -> int:
ensure_directory(temp_dir)
except RuntimeError:
temp_dir = target_dir
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix, dir=str(temp_dir))
temp_file = tempfile.NamedTemporaryFile(
delete=False,
suffix=suffix,
dir=str(temp_dir)
)
temp_path = Path(temp_file.name)
temp_file.close()
downloaded_bytes = 0
@@ -1090,7 +1291,12 @@ def hydrus_export(args, _parser) -> int:
"Hydrus-Client-API-Access-Key": args.access_key,
}
try:
downloaded_bytes = download_hydrus_file(args.file_url, headers, temp_path, args.timeout)
downloaded_bytes = download_hydrus_file(
args.file_url,
headers,
temp_path,
args.timeout
)
if os.environ.get("DOWNLOW_DEBUG"):
log(f"hydrus-export downloaded {downloaded_bytes} bytes", file=sys.stderr)
except httpx.RequestError as exc:
@@ -1139,20 +1345,24 @@ def hydrus_export(args, _parser) -> int:
if completed.returncode != 0:
error_details = ffmpeg_log or (completed.stdout or "").strip()
raise RuntimeError(
f"ffmpeg failed with exit code {completed.returncode}"
+ (f": {error_details}" if error_details else "")
f"ffmpeg failed with exit code {completed.returncode}" +
(f": {error_details}" if error_details else "")
)
shutil.move(str(converted_tmp), str(final_target))
result_path = final_target
apply_mutagen_metadata(result_path, ffmpeg_metadata, args.format)
result_size = result_path.stat().st_size if result_path.exists() else None
payload: dict[str, object] = {"output": str(result_path)}
payload: dict[str,
object] = {
"output": str(result_path)
}
if downloaded_bytes:
payload["source_bytes"] = downloaded_bytes
if result_size is not None:
payload["size_bytes"] = result_size
if metadata_payload:
payload["metadata_keys"] = sorted(ffmpeg_metadata.keys()) if ffmpeg_metadata else []
payload["metadata_keys"] = sorted(ffmpeg_metadata.keys()
) if ffmpeg_metadata else []
log(json.dumps(payload, ensure_ascii=False))
if ffmpeg_log:
log(ffmpeg_log, file=sys.stderr)
@@ -1179,7 +1389,6 @@ def hydrus_export(args, _parser) -> int:
# This section consolidates functions formerly in hydrus_wrapper.py
# Provides: supported filetypes, client initialization, caching, service resolution
# Official Hydrus supported filetypes
# Source: https://hydrusnetwork.github.io/hydrus/filetypes.html
SUPPORTED_FILETYPES = {
@@ -1240,9 +1449,11 @@ SUPPORTED_FILETYPES = {
".pdf": "application/pdf",
".epub": "application/epub+zip",
".djvu": "image/vnd.djvu",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".docx":
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".pptx":
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
".doc": "application/msword",
".xls": "application/vnd.ms-excel",
".ppt": "application/vnd.ms-powerpoint",
@@ -1271,9 +1482,9 @@ SUPPORTED_FILETYPES = {
# Flatten to get all supported extensions
ALL_SUPPORTED_EXTENSIONS = set(GLOBAL_SUPPORTED_EXTENSIONS)
# Global Hydrus client cache to reuse session keys
_hydrus_client_cache: dict[str, Any] = {}
_hydrus_client_cache: dict[str,
Any] = {}
# Cache Hydrus availability across the session
_HYDRUS_AVAILABLE: Optional[bool] = None
@@ -1287,7 +1498,10 @@ def reset_cache() -> None:
_HYDRUS_UNAVAILABLE_REASON = None
def is_available(config: dict[str, Any], use_cache: bool = True) -> tuple[bool, Optional[str]]:
def is_available(config: dict[str,
Any],
use_cache: bool = True) -> tuple[bool,
Optional[str]]:
"""Check if Hydrus is available and accessible.
Performs a lightweight probe to verify:
@@ -1310,7 +1524,7 @@ def is_available(config: dict[str, Any], use_cache: bool = True) -> tuple[bool,
return _HYDRUS_AVAILABLE, _HYDRUS_UNAVAILABLE_REASON
# Use new config helpers first, fallback to old method
from config import get_hydrus_url, get_hydrus_access_key
from SYS.config import get_hydrus_url, get_hydrus_access_key
url = (get_hydrus_url(config, "home") or "").strip()
if not url:
@@ -1399,7 +1613,7 @@ def get_client(config: dict[str, Any]) -> HydrusNetwork:
if not available:
raise RuntimeError(f"Hydrus is unavailable: {reason}")
from config import get_hydrus_url, get_hydrus_access_key
from SYS.config import get_hydrus_url, get_hydrus_access_key
# Use new config helpers
hydrus_url = (get_hydrus_url(config, "home") or "").strip()
@@ -1446,7 +1660,8 @@ def get_tag_service_name(config: dict[str, Any]) -> str:
return "my tags"
def get_tag_service_key(client: HydrusNetwork, fallback_name: str = "my tags") -> Optional[str]:
def get_tag_service_key(client: HydrusNetwork,
fallback_name: str = "my tags") -> Optional[str]:
"""Get the service key for a named tag service.
Queries the Hydrus client's services and finds the service key matching
@@ -1498,7 +1713,11 @@ CHUNK_SIZE = 1024 * 1024 # 1 MiB
def download_hydrus_file(
file_url: str, headers: dict[str, str], destination: Path, timeout: float
file_url: str,
headers: dict[str,
str],
destination: Path,
timeout: float
) -> int:
"""Download *file_url* into *destination* returning the byte count with progress bar."""
from SYS.progress import print_progress, print_final_progress