Add folder-organizer-v3.5: full match depth check, type guards, specific exception handling
This commit is contained in:
parent
4ad1cbd580
commit
b1d3b004ab
1 changed files with 141 additions and 0 deletions
141
folder-organizer-v3.5.py
Normal file
141
folder-organizer-v3.5.py
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
# ================================================================
|
||||||
|
# AMPP Framelight X — Nested Prefix Folder Organizer
|
||||||
|
# Production v3.5 — Delimiter: --
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# "BMG--Videos--File.mp4" → BMG / Videos / (asset linked)
|
||||||
|
# "NEWS--PKG--Clip1.mxf" → NEWS / PKG / (asset linked)
|
||||||
|
# "IMG--4709.jpg" → IMG / (asset linked)
|
||||||
|
# "No-Delimiter.mp4" → skipped
|
||||||
|
#
|
||||||
|
# v3.3 Fix: URL-encode folder paths (special chars: apostrophes, &, etc.)
|
||||||
|
#
|
||||||
|
# v3.4 Fix: Use len() not .Count; use .get() on hierarchy response.
|
||||||
|
#
|
||||||
|
# v3.4.1 Fix: Removed logger.log() (unknown signature).
|
||||||
|
#
|
||||||
|
# v3.5 Fixes:
|
||||||
|
# 1. Validate resp is a dict before calling .get() — avoids AttributeError
|
||||||
|
# if sendAsync returns None or non-dict on error.
|
||||||
|
# 2. Validate hlist is a list before indexing — avoids TypeError if API
|
||||||
|
# returns unexpected structure.
|
||||||
|
# 3. Validate the hierarchy lookup is a FULL match (returned depth matches
|
||||||
|
# expected path depth) before trusting hlist[-1]. A partial match
|
||||||
|
# (API returns A, A/B when we need A/B/C) was previously treated as a
|
||||||
|
# hit, setting parent_id to the wrong level — then the next iteration
|
||||||
|
# would still try to create C, but with the correct parent, so this
|
||||||
|
# was mostly harmless — EXCEPT it would also falsely set folder_id
|
||||||
|
# for the current level and skip creation when C doesn't exist.
|
||||||
|
# Now we count the segments returned vs expected and only trust the
|
||||||
|
# result when they match exactly.
|
||||||
|
# 4. Specific exception handling on hierarchy lookup (was bare except).
|
||||||
|
# Bare except caused silent swallowing of ANY error (network, auth,
|
||||||
|
# malformed response), causing folder creation to run every time when
|
||||||
|
# the API was misbehaving, leading to duplicate folders.
|
||||||
|
# 5. Asset link failure is now logged via raise rather than silently
|
||||||
|
# swallowed — lets the AMPP job log show when linking failed.
|
||||||
|
# 6. Defensive strip on folder IDs returned from API.
|
||||||
|
# ================================================================
|
||||||
|
import json
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
PREFIX_DELIM = "--"
|
||||||
|
asset_id = str(asset.Id)
|
||||||
|
asset_name = str(job.AssetName)
|
||||||
|
|
||||||
|
if PREFIX_DELIM not in asset_name:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
parts = asset_name.split(PREFIX_DELIM)
|
||||||
|
if len(parts) < 2:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
folder_names = parts[:-1]
|
||||||
|
parent_id = None
|
||||||
|
current_path = ""
|
||||||
|
expected_depth = 0 # how many path segments we've built so far
|
||||||
|
|
||||||
|
for fname in folder_names:
|
||||||
|
fname = fname.strip()
|
||||||
|
if not fname:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current_path:
|
||||||
|
current_path = current_path + "/" + fname
|
||||||
|
else:
|
||||||
|
current_path = fname
|
||||||
|
|
||||||
|
expected_depth += 1
|
||||||
|
folder_id = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
encoded = urllib.parse.quote(current_path, safe="/")
|
||||||
|
resp = httpClient.sendAsync(
|
||||||
|
"GET",
|
||||||
|
"api/v1/store/folder/folders/hierarchy?path=" + encoded
|
||||||
|
)
|
||||||
|
|
||||||
|
# Guard: resp must be a dict
|
||||||
|
if not isinstance(resp, dict):
|
||||||
|
raise ValueError("Hierarchy resp is not a dict: " + str(type(resp)))
|
||||||
|
|
||||||
|
hlist = resp.get("hierarchy:list", [])
|
||||||
|
|
||||||
|
# Guard: hlist must be a list
|
||||||
|
if not isinstance(hlist, list):
|
||||||
|
raise ValueError("hierarchy:list is not a list: " + str(type(hlist)))
|
||||||
|
|
||||||
|
# Only trust the result if the depth matches exactly.
|
||||||
|
# A partial match (API returns fewer levels than expected)
|
||||||
|
# means the full path doesn't exist yet — fall through to create.
|
||||||
|
if len(hlist) == expected_depth:
|
||||||
|
candidate_id = str(hlist[-1].get("folder:id", "")).strip()
|
||||||
|
if candidate_id:
|
||||||
|
folder_id = candidate_id
|
||||||
|
|
||||||
|
except Exception as lookup_ex:
|
||||||
|
# Don't silently swallow — but also don't hard-fail the job.
|
||||||
|
# Fall through to creation, which will raise if it also fails.
|
||||||
|
folder_id = None
|
||||||
|
# Uncomment for debug logging if your environment supports it:
|
||||||
|
# raise Exception("Hierarchy lookup failed for '" + current_path + "': " + str(lookup_ex))
|
||||||
|
|
||||||
|
if not folder_id:
|
||||||
|
create_body = {"name:text": fname}
|
||||||
|
if parent_id:
|
||||||
|
create_body["parentFolders:tags"] = [parent_id]
|
||||||
|
|
||||||
|
try:
|
||||||
|
fresp = httpClient.sendAsync(
|
||||||
|
"POST",
|
||||||
|
"api/v1/store/folder/folders",
|
||||||
|
json.dumps(create_body)
|
||||||
|
)
|
||||||
|
# Guard: fresp must be a dict with folder:id
|
||||||
|
if not isinstance(fresp, dict):
|
||||||
|
raise ValueError("Folder create resp is not a dict: " + str(type(fresp)))
|
||||||
|
created_id = str(fresp.get("folder:id", "")).strip()
|
||||||
|
if not created_id:
|
||||||
|
raise ValueError("Folder create resp missing folder:id")
|
||||||
|
folder_id = created_id
|
||||||
|
except Exception as ex:
|
||||||
|
raise Exception(
|
||||||
|
"Failed to create folder '" + fname +
|
||||||
|
"' under path '" + current_path + "': " + str(ex)
|
||||||
|
)
|
||||||
|
|
||||||
|
parent_id = folder_id
|
||||||
|
|
||||||
|
if parent_id:
|
||||||
|
link_body = json.dumps(
|
||||||
|
{"folder:id": parent_id, "asset:id": asset_id},
|
||||||
|
separators=(',', ':')
|
||||||
|
)
|
||||||
|
# Raise on failure so the AMPP job log shows when linking failed.
|
||||||
|
# If the API returns a "duplicate link" error and that's acceptable,
|
||||||
|
# wrap in a specific except for that error code only.
|
||||||
|
httpClient.sendAsync(
|
||||||
|
"POST",
|
||||||
|
"api/v1/store/folder/references",
|
||||||
|
link_body
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue