222 lines
7.6 KiB
GDScript
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
|