Add working hours and scheduled publishing
This commit is contained in:
1 parent
af59e9b83d
commit
451e318860
2 files changed
+150
-6
No files matched your search
@@ -12,6 +12,7 @@ from collections import deque
|
|||||||
from datetime import date, datetime, timedelta, timezone
|
from datetime import date, datetime, timedelta, timezone
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
@@ -79,6 +80,10 @@ GLOBAL_DEFAULTS = {
|
|||||||
"target_seconds": 60,
|
"target_seconds": 60,
|
||||||
"clip_seconds": 8,
|
"clip_seconds": 8,
|
||||||
"error_cooldown_minutes": 5,
|
"error_cooldown_minutes": 5,
|
||||||
|
"work_hours_enabled": False,
|
||||||
|
"work_days": "mon,tue,wed,thu,fri,sat,sun",
|
||||||
|
"work_start": "09:00",
|
||||||
|
"work_end": "17:00",
|
||||||
}
|
}
|
||||||
|
|
||||||
PROFILE_DEFAULTS = {
|
PROFILE_DEFAULTS = {
|
||||||
@@ -99,6 +104,12 @@ PROFILE_DEFAULTS = {
|
|||||||
"trending_count": 25,
|
"trending_count": 25,
|
||||||
"videos_per_day": 0,
|
"videos_per_day": 0,
|
||||||
"minutes_between_videos": 0,
|
"minutes_between_videos": 0,
|
||||||
|
"publish_mode": "immediate",
|
||||||
|
"publish_timezone": "America/New_York",
|
||||||
|
"publish_days": "mon,tue,wed,thu,fri,sat,sun",
|
||||||
|
"publish_time_1": "08:00",
|
||||||
|
"publish_time_2": "",
|
||||||
|
"publish_time_3": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
KIDS_RULES = """The audience is young children under 13. Follow every one of these rules:
|
KIDS_RULES = """The audience is young children under 13. Follow every one of these rules:
|
||||||
@@ -587,6 +598,91 @@ def migrate():
|
|||||||
logger.info("Created Default profile from existing settings")
|
logger.info("Created Default profile from existing settings")
|
||||||
|
|
||||||
|
|
||||||
|
DAY_KEYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_hm(value):
|
||||||
|
m = re.fullmatch(r"(\d{1,2}):(\d{2})", str(value or "").strip())
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
h, mi = int(m.group(1)), int(m.group(2))
|
||||||
|
if h > 23 or mi > 59:
|
||||||
|
return None
|
||||||
|
return h, mi
|
||||||
|
|
||||||
|
|
||||||
|
def day_set(value):
|
||||||
|
return {d.strip() for d in str(value or "").lower().split(",") if d.strip() in DAY_KEYS}
|
||||||
|
|
||||||
|
|
||||||
|
def work_window(s, now=None):
|
||||||
|
if not s["work_hours_enabled"]:
|
||||||
|
return True, None
|
||||||
|
now = now or datetime.now()
|
||||||
|
days = day_set(s["work_days"])
|
||||||
|
if not days:
|
||||||
|
return False, None
|
||||||
|
start = parse_hm(s["work_start"]) or (0, 0)
|
||||||
|
end = parse_hm(s["work_end"]) or (23, 59)
|
||||||
|
t = (now.hour, now.minute)
|
||||||
|
today = DAY_KEYS[now.weekday()]
|
||||||
|
yesterday = DAY_KEYS[(now.weekday() - 1) % 7]
|
||||||
|
if start < end:
|
||||||
|
inside = today in days and start <= t < end
|
||||||
|
else:
|
||||||
|
inside = (t >= start and today in days) or (t < end and yesterday in days)
|
||||||
|
if inside:
|
||||||
|
return True, None
|
||||||
|
for offset in range(0, 8):
|
||||||
|
d = now.date() + timedelta(days=offset)
|
||||||
|
cand = datetime(d.year, d.month, d.day, start[0], start[1])
|
||||||
|
if cand > now and DAY_KEYS[cand.weekday()] in days:
|
||||||
|
return False, cand
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def publish_zone(s):
|
||||||
|
try:
|
||||||
|
return ZoneInfo(s["publish_timezone"] or "America/New_York")
|
||||||
|
except Exception:
|
||||||
|
return ZoneInfo("America/New_York")
|
||||||
|
|
||||||
|
|
||||||
|
def next_publish_slot(s):
|
||||||
|
if s["publish_mode"] != "scheduled":
|
||||||
|
return None
|
||||||
|
times = sorted({hm for hm in (parse_hm(s[k]) for k in ("publish_time_1", "publish_time_2", "publish_time_3")) if hm})
|
||||||
|
days = day_set(s["publish_days"])
|
||||||
|
if not times or not days:
|
||||||
|
return None
|
||||||
|
tz = publish_zone(s)
|
||||||
|
now = datetime.now(tz)
|
||||||
|
earliest = now + timedelta(minutes=30)
|
||||||
|
taken = set(load_state().get("scheduled", {}).get(s["profile_id"], []))
|
||||||
|
for offset in range(0, 90):
|
||||||
|
d = (now + timedelta(days=offset)).date()
|
||||||
|
if DAY_KEYS[d.weekday()] not in days:
|
||||||
|
continue
|
||||||
|
for h, m in times:
|
||||||
|
slot = datetime(d.year, d.month, d.day, h, m, tzinfo=tz)
|
||||||
|
if slot < earliest:
|
||||||
|
continue
|
||||||
|
iso = slot.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
if iso not in taken:
|
||||||
|
return iso
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def record_slot(pid, iso):
|
||||||
|
with state_lock:
|
||||||
|
st = load_state()
|
||||||
|
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
slots = [x for x in st.setdefault("scheduled", {}).get(pid, []) if x > now_iso]
|
||||||
|
slots.append(iso)
|
||||||
|
st["scheduled"][pid] = sorted(set(slots))
|
||||||
|
save_state(st)
|
||||||
|
|
||||||
|
|
||||||
def load_state():
|
def load_state():
|
||||||
with state_lock:
|
with state_lock:
|
||||||
st = read_json(STATE_FILE, {})
|
st = read_json(STATE_FILE, {})
|
||||||
@@ -1444,9 +1540,20 @@ def upload_video(s, path, script, tags):
|
|||||||
description = re.sub(r"[<>]", "", f"{script['description']}\n\n#Shorts {' '.join(hashtags)}").strip()[:4500]
|
description = re.sub(r"[<>]", "", f"{script['description']}\n\n#Shorts {' '.join(hashtags)}").strip()[:4500]
|
||||||
privacy = s["privacy_status"]
|
privacy = s["privacy_status"]
|
||||||
held = kids and s["kids_manual_review"]
|
held = kids and s["kids_manual_review"]
|
||||||
|
publish_at = None
|
||||||
if held:
|
if held:
|
||||||
privacy = "private"
|
privacy = "private"
|
||||||
logger.info("Holding made-for-kids video as private for manual review")
|
logger.info("Holding made-for-kids video as private for manual review")
|
||||||
|
if s["publish_mode"] == "scheduled":
|
||||||
|
logger.info("Publishing schedule skipped because manual review hold is on")
|
||||||
|
else:
|
||||||
|
publish_at = next_publish_slot(s)
|
||||||
|
if publish_at:
|
||||||
|
privacy = "private"
|
||||||
|
local = datetime.strptime(publish_at, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc).astimezone(publish_zone(s))
|
||||||
|
logger.info("Scheduled to go public %s", local.strftime("%a %b %d %I:%M %p %Z"))
|
||||||
|
elif s["publish_mode"] == "scheduled":
|
||||||
|
logger.warning("Publishing schedule has no valid days or times, uploading as %s", privacy)
|
||||||
body = {
|
body = {
|
||||||
"snippet": {
|
"snippet": {
|
||||||
"title": title,
|
"title": title,
|
||||||
@@ -1460,6 +1567,8 @@ def upload_video(s, path, script, tags):
|
|||||||
"containsSyntheticMedia": bool(s["ai_disclosure"]),
|
"containsSyntheticMedia": bool(s["ai_disclosure"]),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if publish_at:
|
||||||
|
body["status"]["publishAt"] = publish_at
|
||||||
logger.info(
|
logger.info(
|
||||||
"Uploading to %s as category %s, made for kids: %s, AI disclosure: %s",
|
"Uploading to %s as category %s, made for kids: %s, AI disclosure: %s",
|
||||||
profile_channel(s["profile_id"]), body["snippet"]["categoryId"], kids, bool(s["ai_disclosure"]),
|
profile_channel(s["profile_id"]), body["snippet"]["categoryId"], kids, bool(s["ai_disclosure"]),
|
||||||
@@ -1473,7 +1582,9 @@ def upload_video(s, path, script, tags):
|
|||||||
if prog:
|
if prog:
|
||||||
logger.info("Upload %d%%", int(prog.progress() * 100))
|
logger.info("Upload %d%%", int(prog.progress() * 100))
|
||||||
logger.info("Uploaded: https://youtube.com/shorts/%s (%s)", resp["id"], privacy)
|
logger.info("Uploaded: https://youtube.com/shorts/%s (%s)", resp["id"], privacy)
|
||||||
return resp["id"], held
|
if publish_at:
|
||||||
|
record_slot(s["profile_id"], publish_at)
|
||||||
|
return resp["id"], held, publish_at
|
||||||
|
|
||||||
|
|
||||||
def run_job(s):
|
def run_job(s):
|
||||||
@@ -1620,7 +1731,8 @@ def run_job(s):
|
|||||||
final = job_dir / "final.mp4"
|
final = job_dir / "final.mp4"
|
||||||
concat_clips(clips, final, s["aspect_ratio"])
|
concat_clips(clips, final, s["aspect_ratio"])
|
||||||
set_stage("uploading to YouTube", step=4 + n * 2)
|
set_stage("uploading to YouTube", step=4 + n * 2)
|
||||||
vid, held = upload_video(s, final, script, tags)
|
vid, held, publish_at = upload_video(s, final, script, tags)
|
||||||
|
compliance["publish_at"] = publish_at
|
||||||
compliance["made_for_kids_flag"] = kids
|
compliance["made_for_kids_flag"] = kids
|
||||||
compliance["synthetic_media_flag"] = bool(s["ai_disclosure"])
|
compliance["synthetic_media_flag"] = bool(s["ai_disclosure"])
|
||||||
compliance["held_for_review"] = held
|
compliance["held_for_review"] = held
|
||||||
@@ -1639,7 +1751,8 @@ def run_job(s):
|
|||||||
c.unlink(missing_ok=True)
|
c.unlink(missing_ok=True)
|
||||||
set_stage("moving files")
|
set_stage("moving files")
|
||||||
location = finish_files(s, job_dir, job_id)
|
location = finish_files(s, job_dir, job_id)
|
||||||
update_history(job_id, status="review" if held else "published", youtube_id=vid, location=location, finished_ts=int(datetime.now().timestamp()))
|
final_status = "review" if held else ("scheduled" if publish_at else "published")
|
||||||
|
update_history(job_id, status=final_status, youtube_id=vid, location=location, publish_at=publish_at, finished_ts=int(datetime.now().timestamp()))
|
||||||
set_stage("job complete", step=status["steps"])
|
set_stage("job complete", step=status["steps"])
|
||||||
except Cancelled:
|
except Cancelled:
|
||||||
update_history(job_id, status="cancelled", finished_ts=int(datetime.now().timestamp()))
|
update_history(job_id, status="cancelled", finished_ts=int(datetime.now().timestamp()))
|
||||||
@@ -1659,6 +1772,14 @@ def run_loop():
|
|||||||
try:
|
try:
|
||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
s = load_settings()
|
s = load_settings()
|
||||||
|
ok, resume = work_window(s)
|
||||||
|
if not ok:
|
||||||
|
msg = f"outside working hours, resumes {resume.strftime('%a %I:%M %p')}" if resume else "outside working hours, no work days selected"
|
||||||
|
if status["stage"] != msg:
|
||||||
|
logger.info("Outside working hours, %s", msg.split(", ", 1)[1])
|
||||||
|
set_stage(msg, log=False)
|
||||||
|
stop_event.wait(60)
|
||||||
|
continue
|
||||||
limit = int(s["videos_per_day"])
|
limit = int(s["videos_per_day"])
|
||||||
if limit > 0 and published_today(s["profile_id"]) >= limit:
|
if limit > 0 and published_today(s["profile_id"]) >= limit:
|
||||||
if status["stage"] != "daily limit reached":
|
if status["stage"] != "daily limit reached":
|
||||||
|
|||||||
+26
-3
@@ -47,7 +47,7 @@ input,select{width:100%;padding:8px;background:#0f1115;border:1px solid var(--bo
|
|||||||
.hint.notes{color:#ffb74d;word-break:normal}
|
.hint.notes{color:#ffb74d;word-break:normal}
|
||||||
a{color:#64b5f6}
|
a{color:#64b5f6}
|
||||||
.pill{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;background:#394150}
|
.pill{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;background:#394150}
|
||||||
.pill.published{background:#1b5e20}.pill.failed{background:#b71c1c}.pill.running{background:#e65100}.pill.review{background:#6a1b9a}
|
.pill.published{background:#1b5e20}.pill.failed{background:#b71c1c}.pill.running{background:#e65100}.pill.review{background:#6a1b9a}.pill.scheduled{background:#1565c0}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -130,6 +130,18 @@ const FIELDS=[
|
|||||||
["trending_count","Videos to scan","select:10|25|50","","p"],
|
["trending_count","Videos to scan","select:10|25|50","","p"],
|
||||||
["videos_per_day","Videos per day","select:0=Unlimited|1|2|3|4|5|6|8|10|12|15|20","The default YouTube API quota allows about 6 uploads a day. Higher values need a quota increase from Google","p"],
|
["videos_per_day","Videos per day","select:0=Unlimited|1|2|3|4|5|6|8|10|12|15|20","The default YouTube API quota allows about 6 uploads a day. Higher values need a quota increase from Google","p"],
|
||||||
["minutes_between_videos","Minutes between videos","select:0=No gap|30|60|120|180|240|360","","p"],
|
["minutes_between_videos","Minutes between videos","select:0=No gap|30|60|120|180|240|360","","p"],
|
||||||
|
["","Publishing schedule","header","",""],
|
||||||
|
["publish_mode","When to publish","select:immediate=As soon as it is uploaded|scheduled=At set days and times","Scheduled videos upload as private and YouTube makes them public at the next open time slot","p"],
|
||||||
|
["publish_timezone","Publishing time zone","select:America/New_York=Eastern (US and Canada)|America/Chicago=Central (US and Canada)|America/Denver=Mountain (US and Canada)|America/Phoenix=Arizona|America/Los_Angeles=Pacific (US and Canada)|America/Anchorage=Alaska|Pacific/Honolulu=Hawaii|America/Halifax=Atlantic (Canada)|America/Mexico_City=Mexico City|Europe/London=London|Europe/Berlin=Central Europe|Asia/Kolkata=India|Australia/Sydney=Sydney|UTC=UTC","Times below are in this zone, pick your audience's","p"],
|
||||||
|
["publish_days","Publishing days","days","","p"],
|
||||||
|
["publish_time_1","Publish time 1","time","","p"],
|
||||||
|
["publish_time_2","Publish time 2","time:opt","Optional","p"],
|
||||||
|
["publish_time_3","Publish time 3","time:opt","Optional","p"],
|
||||||
|
["","Working hours (shared by all profiles)","header","",""],
|
||||||
|
["work_hours_enabled","Only work during set hours","checkbox","Outside these hours no new video is started. A video already in progress finishes first. Uses this computer's clock","g"],
|
||||||
|
["work_days","Working days","days","","g"],
|
||||||
|
["work_start","Start work at","time","","g"],
|
||||||
|
["work_end","Stop starting new videos at","time","If this is earlier than the start time, the window runs overnight","g"],
|
||||||
["","Folders (shared by all profiles)","header","",""],
|
["","Folders (shared by all profiles)","header","",""],
|
||||||
["storage_dir","Storage folder","wide","Full path on a large disk, e.g. /Volumes/iDrive/shorts-autopilot. Jobs are built in output/ and moved to published/","g"],
|
["storage_dir","Storage folder","wide","Full path on a large disk, e.g. /Volumes/iDrive/shorts-autopilot. Jobs are built in output/ and moved to published/","g"],
|
||||||
["models_dir","Models folder","wide","Where AI models download to (several GB). Blank puts them in the storage folder","g"],
|
["models_dir","Models folder","wide","Where AI models download to (several GB). Blank puts them in the storage folder","g"],
|
||||||
@@ -160,6 +172,8 @@ const FIELDS=[
|
|||||||
["clip_seconds","Seconds per scene","select:5|6|8|10|12","8 gives 7 scenes in a 60 second Short","g"],
|
["clip_seconds","Seconds per scene","select:5|6|8|10|12","8 gives 7 scenes in a 60 second Short","g"],
|
||||||
["error_cooldown_minutes","Wait after error (minutes)","select:1|5|10|15|30|60","","g"]
|
["error_cooldown_minutes","Wait after error (minutes)","select:1|5|10|15|30|60","","g"]
|
||||||
];
|
];
|
||||||
|
const DAYS=[["mon","Mon"],["tue","Tue"],["wed","Wed"],["thu","Thu"],["fri","Fri"],["sat","Sat"],["sun","Sun"]];
|
||||||
|
const TIMES=[];for(let h=0;h<24;h++)for(const m of [0,30]){const v=String(h).padStart(2,"0")+":"+String(m).padStart(2,"0");const hr=h%12||12;TIMES.push([v,hr+":"+String(m).padStart(2,"0")+(h<12?" AM":" PM")])}
|
||||||
const $=s=>document.querySelector(s);
|
const $=s=>document.querySelector(s);
|
||||||
const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
||||||
async function api(path,opts={}){const r=await fetch(path,{headers:{"Content-Type":"application/json"},...opts});const j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.detail||r.statusText);return j}
|
async function api(path,opts={}){const r=await fetch(path,{headers:{"Content-Type":"application/json"},...opts});const j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.detail||r.statusText);return j}
|
||||||
@@ -177,6 +191,8 @@ const s=scope==="g"?data.global:data.profile;
|
|||||||
let input;
|
let input;
|
||||||
if(type.startsWith("select:")){const list=type.slice(7).split("|").map(opt);const cur=String(s[k]??"");if(!list.some(([v])=>v===cur))list.unshift([cur,cur+" (current)"]);input=`<select id="f-${k}">${list.map(([v,l])=>`<option value="${esc(v)}" ${cur===v?"selected":""}>${esc(l)}</option>`).join("")}</select>`}
|
if(type.startsWith("select:")){const list=type.slice(7).split("|").map(opt);const cur=String(s[k]??"");if(!list.some(([v])=>v===cur))list.unshift([cur,cur+" (current)"]);input=`<select id="f-${k}">${list.map(([v,l])=>`<option value="${esc(v)}" ${cur===v?"selected":""}>${esc(l)}</option>`).join("")}</select>`}
|
||||||
else if(type==="checkbox"){input=`<input type="checkbox" id="f-${k}" ${s[k]?"checked":""} style="width:auto">`}
|
else if(type==="checkbox"){input=`<input type="checkbox" id="f-${k}" ${s[k]?"checked":""} style="width:auto">`}
|
||||||
|
else if(type==="days"){const on=new Set(String(s[k]||"").split(","));input=`<div class="row" id="f-${k}" data-days="1" style="gap:10px">${DAYS.map(([v,l])=>`<label style="margin:0;display:flex;gap:4px;align-items:center;color:var(--text)"><input type="checkbox" value="${v}" ${on.has(v)?"checked":""} style="width:auto">${l}</label>`).join("")}</div>`}
|
||||||
|
else if(type.startsWith("time")){const cur=String(s[k]||"");const opts=(type==="time:opt"?[["","None"]]:[]).concat(TIMES);if(cur&&!opts.some(([v])=>v===cur))opts.push([cur,cur]);input=`<select id="f-${k}">${opts.map(([v,l])=>`<option value="${esc(v)}" ${cur===v?"selected":""}>${esc(l)}</option>`).join("")}</select>`}
|
||||||
else if(type.startsWith("model:")){
|
else if(type.startsWith("model:")){
|
||||||
const kind=type.slice(6);
|
const kind=type.slice(6);
|
||||||
const installed=data.installed_models||[];
|
const installed=data.installed_models||[];
|
||||||
@@ -201,6 +217,11 @@ const kids=$("#f-made_for_kids");
|
|||||||
if(kids){
|
if(kids){
|
||||||
for(const id of ["#f-kids_manual_review","#f-kids_llm_review"]){const el=$(id);if(!el)continue;el.disabled=!kids.checked;el.closest("div").style.opacity=kids.checked?1:.5}
|
for(const id of ["#f-kids_manual_review","#f-kids_llm_review"]){const el=$(id);if(!el)continue;el.disabled=!kids.checked;el.closest("div").style.opacity=kids.checked?1:.5}
|
||||||
kids.onchange=syncFields}
|
kids.onchange=syncFields}
|
||||||
|
const dim=(ids,off)=>{for(const id of ids){const el=$(id);if(!el)continue;el.querySelectorAll?el.querySelectorAll("input").forEach(x=>x.disabled=off):0;if(el.tagName!=="DIV")el.disabled=off;el.closest(".grid > div").style.opacity=off?.5:1}};
|
||||||
|
const wh=$("#f-work_hours_enabled");
|
||||||
|
if(wh){dim(["#f-work_days","#f-work_start","#f-work_end"],!wh.checked);wh.onchange=syncFields}
|
||||||
|
const pm=$("#f-publish_mode");
|
||||||
|
if(pm){dim(["#f-publish_timezone","#f-publish_days","#f-publish_time_1","#f-publish_time_2","#f-publish_time_3"],pm.value!=="scheduled");pm.onchange=syncFields}
|
||||||
const sdm=$("#f-sd_model");
|
const sdm=$("#f-sd_model");
|
||||||
if(sdm&&!sdm.dataset.wired){sdm.dataset.wired="1";sdm.addEventListener("change",()=>{
|
if(sdm&&!sdm.dataset.wired){sdm.dataset.wired="1";sdm.addEventListener("change",()=>{
|
||||||
const best=sdm.value.includes("stable-diffusion-xl-base");
|
const best=sdm.value.includes("stable-diffusion-xl-base");
|
||||||
@@ -215,7 +236,9 @@ eng.onchange=syncFields}}
|
|||||||
async function saveSettings(){
|
async function saveSettings(){
|
||||||
const body={global:{},profile:{}};
|
const body={global:{},profile:{}};
|
||||||
const NUMERIC=new Set(["sd_steps","sd_guidance","sd_width","sd_height","trending_days","trending_count","videos_per_day","minutes_between_videos","target_seconds","clip_seconds","ollama_timeout","error_cooldown_minutes","tts_rate"]);
|
const NUMERIC=new Set(["sd_steps","sd_guidance","sd_width","sd_height","trending_days","trending_count","videos_per_day","minutes_between_videos","target_seconds","clip_seconds","ollama_timeout","error_cooldown_minutes","tts_rate"]);
|
||||||
for(const [k,,type,,scope] of FIELDS){if(type==="header")continue;const el=$("#f-"+k);body[scope==="g"?"global":"profile"][k]=type==="checkbox"?el.checked:(type==="number"||NUMERIC.has(k))?Number(el.value):el.value}
|
for(const [k,,type,,scope] of FIELDS){if(type==="header")continue;const el=$("#f-"+k);const dest=body[scope==="g"?"global":"profile"];
|
||||||
|
if(type==="days"){dest[k]=[...el.querySelectorAll("input:checked")].map(x=>x.value).join(",");continue}
|
||||||
|
dest[k]=type==="checkbox"?el.checked:(type==="number"||NUMERIC.has(k))?Number(el.value):el.value}
|
||||||
try{await api("/api/settings",{method:"POST",body:JSON.stringify(body)});await loadSettings();$("#saved").textContent="Saved "+new Date().toLocaleTimeString();refresh()}catch(e){$("#saved").textContent="Error: "+e.message}}
|
try{await api("/api/settings",{method:"POST",body:JSON.stringify(body)});await loadSettings();$("#saved").textContent="Saved "+new Date().toLocaleTimeString();refresh()}catch(e){$("#saved").textContent="Error: "+e.message}}
|
||||||
async function activate(id){try{await api("/api/profiles/activate",{method:"POST",body:JSON.stringify({id})})}catch(e){alert(e.message)}await loadSettings();refresh()}
|
async function activate(id){try{await api("/api/profiles/activate",{method:"POST",body:JSON.stringify({id})})}catch(e){alert(e.message)}await loadSettings();refresh()}
|
||||||
async function refresh(){
|
async function refresh(){
|
||||||
@@ -242,7 +265,7 @@ $("#pg-meta").textContent=pct+"% | step "+s.step+" of "+s.steps+(s.elapsed?" | "
|
|||||||
const lb=$("#link");lb.textContent=s.channel?"Unlink YouTube Channel":"Link YouTube Channel";lb.dataset.linked=s.channel?"1":"";
|
const lb=$("#link");lb.textContent=s.channel?"Unlink YouTube Channel":"Link YouTube Channel";lb.dataset.linked=s.channel?"1":"";
|
||||||
setButton(s.running);
|
setButton(s.running);
|
||||||
const dur=h=>{if(!h.started_ts)return"";const end=h.finished_ts||(h.status==="running"?s.now:0);if(!end)return"";const d=Math.max(0,end-h.started_ts);const m=Math.floor(d/60),sec=d%60;return(m?m+"m ":"")+sec+"s"};
|
const dur=h=>{if(!h.started_ts)return"";const end=h.finished_ts||(h.status==="running"?s.now:0);if(!end)return"";const d=Math.max(0,end-h.started_ts);const m=Math.floor(d/60),sec=d%60;return(m?m+"m ":"")+sec+"s"};
|
||||||
$("#jobs").innerHTML="<tr><th>Started</th><th>Time</th><th>Profile</th><th>Source</th><th>Title</th><th>Status</th><th>Video</th></tr>"+s.history.map(h=>`<tr><td>${esc(h.started)}</td><td style="white-space:nowrap">${esc(dur(h))}</td><td>${esc(h.profile)}</td><td><a target="_blank" href="https://youtube.com/watch?v=${esc(h.source_id)}">${esc(h.source_title)}</a></td><td>${esc(h.title)}</td><td><span class="pill ${esc(h.status)}">${h.status==="review"?"private, needs review":esc(h.status)}</span>${h.notes?`<div class="hint notes">Reviewer notes: ${esc(h.notes)}</div>`:""}${h.error?`<div class="hint">${esc(h.error)}</div>`:""}${h.location?`<div class="hint">${esc(h.location)}</div>`:""}</td><td>${h.youtube_id?`<a target="_blank" href="https://studio.youtube.com/video/${esc(h.youtube_id)}/edit">studio</a> <a target="_blank" href="https://youtube.com/shorts/${esc(h.youtube_id)}">open</a>`:""}</td></tr>`).join("")
|
$("#jobs").innerHTML="<tr><th>Started</th><th>Time</th><th>Profile</th><th>Source</th><th>Title</th><th>Status</th><th>Video</th></tr>"+s.history.map(h=>`<tr><td>${esc(h.started)}</td><td style="white-space:nowrap">${esc(dur(h))}</td><td>${esc(h.profile)}</td><td><a target="_blank" href="https://youtube.com/watch?v=${esc(h.source_id)}">${esc(h.source_title)}</a></td><td>${esc(h.title)}</td><td><span class="pill ${esc(h.status)}">${h.status==="review"?"private, needs review":h.status==="scheduled"&&h.publish_at?"scheduled "+new Date(h.publish_at).toLocaleString([], {weekday:"short",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):esc(h.status)}</span>${h.notes?`<div class="hint notes">Reviewer notes: ${esc(h.notes)}</div>`:""}${h.error?`<div class="hint">${esc(h.error)}</div>`:""}${h.location?`<div class="hint">${esc(h.location)}</div>`:""}</td><td>${h.youtube_id?`<a target="_blank" href="https://studio.youtube.com/video/${esc(h.youtube_id)}/edit">studio</a> <a target="_blank" href="https://youtube.com/shorts/${esc(h.youtube_id)}">open</a>`:""}</td></tr>`).join("")
|
||||||
}catch(e){}}
|
}catch(e){}}
|
||||||
let since=0;
|
let since=0;
|
||||||
async function pollLogs(){
|
async function pollLogs(){
|
||||||
|
|||||||
Reference in new issue
Block a user