88Scheduling only — going live immediately is deliberately NOT offered here; a wrongly
99timed scheduled post can be deleted, a live post cannot.
1010"""
11+ import json
12+ import subprocess
1113from datetime import date
1214from pathlib import Path
1315
14- from . import api , plan
16+ from . import api , config , plan
1517
1618
1719def _caption (video : Path , caption : str | None ) -> str :
@@ -30,6 +32,22 @@ def _title(video: Path, title: str | None) -> str | None:
3032 return sidecar .read_text ().strip () if sidecar .exists () else None
3133
3234
35+ def _fire_hook (payload : dict ) -> dict | None :
36+ """Run the operator's on_scheduled hook (config) with the schedule result as JSON
37+ on stdin. Fire-and-report: a failing hook is surfaced in the result but NEVER
38+ undoes or fails the schedule itself — the post is already placed."""
39+ cmd = config .load ().get ("on_scheduled" , "" )
40+ if not cmd :
41+ return None
42+ try :
43+ p = subprocess .run (cmd , shell = True , input = json .dumps (payload , ensure_ascii = False ),
44+ capture_output = True , text = True , timeout = 120 )
45+ return {"ok" : p .returncode == 0 ,
46+ ** ({"stderr" : p .stderr [- 300 :]} if p .returncode != 0 else {})}
47+ except Exception as e :
48+ return {"ok" : False , "stderr" : f"{ type (e ).__name__ } : { e } " }
49+
50+
3351def _already_scheduled (video : Path ) -> str | None :
3452 """Idempotency guard: has THIS file already been scheduled into a future slot?
3553 Retrying a batch after a partial failure must not double-post the successes."""
@@ -76,11 +94,16 @@ def schedule(videos: list, captions: list | None = None, titles: list | None = N
7694 "orphaned_upload" : url , "error" : f"{ type (e ).__name__ } : { e } " })
7795 continue
7896 ok = bool (r .get ("id" ) or r .get ("success" ) or r .get ("postId" ))
97+ result = {"ok" : ok , "video" : video .name , "slot" : slot , "response" : r }
7998 if ok :
80- plan .record (slot , video .name , series ,
81- str (r .get ("postId" ) or r .get ("id" ) or "" ), caption = cap )
99+ pid = str (r .get ("postId" ) or r .get ("id" ) or "" )
100+ plan .record (slot , video .name , series , pid , caption = cap )
101+ hook = _fire_hook ({"video" : str (video ), "slot" : slot , "post_id" : pid ,
102+ "series" : series , "caption" : cap , "media_url" : url })
103+ if hook is not None :
104+ result ["hook" ] = hook
82105 else :
83106 taken .pop (slot , None ) # post failed -> slot is still free
84- r = {** r , "orphaned_upload" : url }
85- results .append ({ "ok" : ok , "video" : video . name , "slot" : slot , "response" : r } )
107+ result [ "response" ] = {** r , "orphaned_upload" : url }
108+ results .append (result )
86109 return results
0 commit comments