Files
scorpion21211-cmd 965f0d7584 Initial commit
2026-10-03 23:29:41 -04:00

222 lines
7.6 KiB
GDScript

@tool
extends RefCounted
class_name CghDownloads
## Cross-addon "downloaded" state — matches the After Effects / Premiere / DaVinci
## addons, which all share ONE manifest file: ~/.cgheven_cg_dl_v2.json
##
## Detection is by ASSET ID (each record is {id, fp, fmt, res, title, cat, at}); an
## asset counts as downloaded if the manifest has a record for its id whose file is
## still on disk. We append a record whenever this addon finishes a download, so a
## file grabbed in Godot shows the ✓ in AE/Premiere/DaVinci too, and vice-versa.
##
## We ALSO keep a cheap filename scan of the shared download folders as a secondary
## signal (this catches the Blender addon, which uses its own ~/Documents/CGHEVEN +
## a separate prefs file and isn't part of the v2 manifest).
const MANIFEST_NAME := ".cgheven_cg_dl_v2.json"
const MAX_RECORDS := 200
static var _names := {} # lower-case filename -> true (folder scan)
static var _ids := {} # asset-id string -> true (manifest, file-exists verified)
static var _id_res := {} # "id|res" -> true (per-resolution, cross-addon)
static var _scanned := false
# Normalize an id so "3535.0" (an old float-string id) matches "3535".
static func _norm_id(v) -> String:
var s := str(v)
if s.ends_with(".0"):
s = s.substr(0, s.length() - 2)
return s
static func _manifest_path() -> String:
var home := CghConfig.os_home()
if home == "":
return ""
return home.path_join(MANIFEST_NAME)
static func _read_manifest() -> Array:
var p := _manifest_path()
if p == "" or not FileAccess.file_exists(p):
return []
var f := FileAccess.open(p, FileAccess.READ)
if f == null:
return []
var txt := f.get_as_text()
f.close()
var d = JSON.parse_string(txt)
return d if d is Array else []
static func _write_manifest(arr: Array) -> void:
var p := _manifest_path()
if p == "":
return
var f := FileAccess.open(p, FileAccess.WRITE)
if f:
f.store_string(JSON.stringify(arr))
f.close()
static func rescan() -> void:
_names.clear()
_ids.clear()
_id_res.clear()
# 1) shared manifest (AE / Premiere / DaVinci) — verify each file still exists
for r in _read_manifest():
if not (r is Dictionary):
continue
var fp := str(r.get("fp", ""))
if fp != "" and FileAccess.file_exists(fp):
var rid := _norm_id(r.get("id", ""))
_ids[rid] = true
_id_res["%s|%s" % [rid, str(r.get("res", ""))]] = true
# 2) folder filename scan (Blender + this addon's own folder)
for dir in CghConfig.shared_download_dirs():
_scan_dir(dir)
_scanned = true
static func _scan_dir(abs_dir: String) -> void:
var da := DirAccess.open(abs_dir)
if da == null:
return
da.list_dir_begin()
var fn := da.get_next()
while fn != "":
if not da.current_is_dir() and not fn.ends_with(".cghpart"):
_names[fn.to_lower()] = true
fn = da.get_next()
da.list_dir_end()
static func _ensure() -> void:
if not _scanned:
rescan()
## True if this asset is already downloaded by ANY CGHEVEN addon.
static func is_downloaded(asset: Dictionary) -> bool:
_ensure()
# id match via the shared manifest (the AE/Premiere/DaVinci scheme; cross-addon)
if _ids.has(_norm_id(CghAsset.id(asset))):
return true
# fallback: any of this asset's files present on disk by name
for e in CghAsset.file_entries(asset):
if e["locked"]:
continue
var nm: String = CghConfig.safe_download_name(e["filename"]).to_lower()
if _names.has(nm):
return true
return false
## True if a SPECIFIC resolution of this asset was downloaded by any addon.
static func is_res_downloaded(asset_id: String, res: String) -> bool:
_ensure()
return _id_res.has("%s|%s" % [_norm_id(asset_id), res])
## True if THIS specific file (by name) is already on disk in a shared folder —
## used to switch a per-resolution menu row from "Download" to "Import".
static func has_file(filename: String) -> bool:
_ensure()
return _names.has(CghConfig.safe_download_name(filename).to_lower())
## Absolute path of this file in ANY shared CGHEVEN folder, or "" if not on disk.
static func local_path(filename: String) -> String:
var safe := CghConfig.safe_download_name(filename)
for d in CghConfig.shared_download_dirs():
var p: String = d.path_join(safe)
if FileAccess.file_exists(p):
return p
return ""
## Record a finished download into BOTH the in-memory caches and the shared manifest
## so every other CGHEVEN addon sees it immediately.
static func record(asset: Dictionary, fp: String, fmt: String, res: String) -> void:
var id := _norm_id(CghAsset.id(asset))
_ids[id] = true
_id_res["%s|%s" % [id, res]] = true
_names[CghConfig.safe_download_name(fp.get_file()).to_lower()] = true
var arr := _read_manifest()
# drop any prior record for the same id+fmt+res, then prepend the new one
var kept := []
for r in arr:
if not (r is Dictionary):
continue
if _norm_id(r.get("id", "")) == id and str(r.get("fmt", "")) == fmt and str(r.get("res", "")) == res:
continue
kept.append(r)
kept.insert(0, {
"id": id,
"fp": fp,
"fmt": fmt,
"res": res,
"title": CghAsset.title(asset),
"cat": CghAsset.category_slug(asset),
"at": int(Time.get_unix_time_from_system() * 1000.0),
})
if kept.size() > MAX_RECORDS:
kept = kept.slice(0, MAX_RECORDS)
_write_manifest(kept)
## Delete every downloaded copy of this asset from the shared folders and drop its
## manifest records, so the card flips back to "Download". Use it to re-fetch a file
## that went missing or imported corrupt. Returns how many files were removed.
static func delete(asset: Dictionary) -> int:
_ensure()
var id := _norm_id(CghAsset.id(asset))
var names := {}
for e in CghAsset.file_entries(asset):
names[CghConfig.safe_download_name(e["filename"]).to_lower()] = true
var removed := 0
# Drop this asset's manifest records, deleting the files they point at first.
var kept := []
for r in _read_manifest():
if not (r is Dictionary):
continue
if _norm_id(r.get("id", "")) == id:
var fp := str(r.get("fp", ""))
if fp != "":
names[fp.get_file().to_lower()] = true
if FileAccess.file_exists(fp) and DirAccess.remove_absolute(fp) == OK:
removed += 1
continue
kept.append(r)
_write_manifest(kept)
# Delete any remaining copies by filename across every shared CGHEVEN folder.
for d in CghConfig.shared_download_dirs():
for nm in names.keys():
var p: String = d.path_join(nm)
if FileAccess.file_exists(p) and DirAccess.remove_absolute(p) == OK:
removed += 1
# Clear the in-memory caches so is_downloaded()/has_file() update immediately.
_ids.erase(id)
for k in _id_res.keys():
if str(k).begins_with(id + "|"):
_id_res.erase(k)
for nm in names.keys():
_names.erase(nm)
return removed
## Back-compat shim — older callers just marked a filename saved.
static func mark_saved(filename: String) -> void:
_names[CghConfig.safe_download_name(filename).to_lower()] = true
## Titles (+ resolutions) of assets present in the shared cross-addon manifest whose
## file is still on disk — used by the ⤓ Sync button to show "what synced". One row
## per asset (resolutions merged), newest first.
static func synced_records() -> Array:
var by_id := {} # id -> {title, cat, res:[...]}
var order := [] # id first-seen order (manifest is newest-first)
for r in _read_manifest():
if not (r is Dictionary):
continue
var fp := str(r.get("fp", ""))
if fp == "" or not FileAccess.file_exists(fp):
continue
var id := _norm_id(r.get("id", ""))
if not by_id.has(id):
by_id[id] = {"title": str(r.get("title", "")), "cat": str(r.get("cat", "")), "res": []}
order.append(id)
var res := str(r.get("res", ""))
if res != "" and not (res in by_id[id]["res"]):
by_id[id]["res"].append(res)
var out := []
for id in order:
out.append(by_id[id])
return out