Initial commit
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
@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
|
||||
Reference in New Issue
Block a user