Spaces:
Running
Running
File size: 22,600 Bytes
9c10fcb ce0ef24 9c10fcb 17fefbc 9c10fcb ce0ef24 9c10fcb 17fefbc ce0ef24 9c10fcb ce0ef24 9c10fcb ce0ef24 9c10fcb 17fefbc 9c10fcb ce0ef24 9c10fcb ce0ef24 9c10fcb ce0ef24 9c10fcb ce0ef24 9c10fcb 17fefbc 9c10fcb ce0ef24 9c10fcb ce0ef24 9c10fcb ce0ef24 9c10fcb 17fefbc ce0ef24 17fefbc ce0ef24 9c10fcb ce0ef24 9c10fcb ce0ef24 9c10fcb 17fefbc ce0ef24 9c10fcb ce0ef24 9c10fcb ce0ef24 9c10fcb d86bec8 047a82a d86bec8 9c10fcb ce0ef24 9c10fcb ce0ef24 c92cb2e 9c10fcb ce0ef24 9c10fcb 17fefbc 9c10fcb 17fefbc ce0ef24 9c10fcb ce0ef24 9c10fcb 17fefbc 9c10fcb ce0ef24 17fefbc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | <!doctype html><html lang="ar" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>TikTok Live Monitor</title>
<style>
:root{color-scheme:dark;font-family:system-ui,sans-serif}
body{margin:0;background:#0b1220;color:#e6edf7}
main{max-width:1080px;margin:32px auto;padding:0 18px}
h1{font-size:28px;margin:0}h2{font-size:18px;margin:0 0 12px}
p,small{color:#9db0c9;line-height:1.7}
.panel{background:#141c2c;border:1px solid #2a3850;border-radius:16px;padding:20px;margin:18px 0}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px}
.card{border:1px solid #31425c;border-radius:12px;padding:16px}
.card h3{margin:0 0 6px;direction:ltr;text-align:right}
.row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
input,button,select,textarea{font:inherit;border-radius:10px;padding:10px 12px;border:1px solid #3a4d68}
input,select,textarea{background:#0b1220;color:#fff;flex:1;min-width:120px}
button{background:#6fe0b4;color:#10251d;font-weight:650;cursor:pointer}
button.secondary{background:#223249;color:#d5e2f5}
button.danger{background:#3e2430;color:#ffb9c8}
.badge{display:inline-block;border:1px solid #355445;border-radius:999px;padding:3px 10px;color:#78e1b7;font-size:12px}
.badge.off{border-color:#5a3a3a;color:#f0b4b4}
.badge.live{border-color:#3d8;color:#7f6}
.kv{display:grid;grid-template-columns:120px 1fr;gap:6px 10px;font-size:14px}
.kv b{color:#c5d4e8;font-weight:600}
#logs{max-height:260px;overflow:auto;font:13px/1.8 ui-monospace,monospace;background:#0b1220;border-radius:10px;padding:10px}
#error{color:#ffc4c4;white-space:pre-line}
label{display:block;margin:10px 0 6px;color:#c5d4e8}
textarea{width:100%;box-sizing:border-box}
details{margin-top:12px}
footer{color:#8a9bb3;font-size:12px;margin:24px 0}
a{color:#79dcc5}
</style></head><body><main>
<header class="row" style="justify-content:space-between">
<div><small>TikTok Live Monitor → Direct Relay → YouTube</small><h1>رصد البث المباشر</h1></div>
<div class="row"><span class="badge" id="sysbadge">System</span><button class="secondary" id="logout" type="button" hidden>خروج</button></div>
</header>
<p id="error" hidden></p>
<section class="panel" id="login-panel">
<h2>دخول الإدارة</h2>
<p>استخدم كلمة السر المحفوظة في Secret باسم <code>PASSWORD</code>. لا تستخدم توكن Hugging Face هنا.</p>
<form id="login" class="row"><input id="admin-password" type="password" autocomplete="current-password" placeholder="كلمة السر" required><button>دخول</button></form>
<p id="login-error" hidden></p>
</section>
<section class="panel" id="accounts-panel" hidden>
<h2>Accounts</h2>
<form id="add" class="row"><input id="username" placeholder="اسم المستخدم أو رابط الحساب" required>
<input id="token" placeholder="توكن sessionid — اختياري للغرف 18+">
<button>إضافة ومراقبة</button></form>
<p class="row"><button class="secondary" id="reload" type="button">تحديث</button><small id="updated"></small></p>
<div id="tasks" class="cards"></div>
<details><summary>توكن تيك توك (للبثوث المقيدة عمريًا)</summary>
<label>الصق كامل سطر Cookie أو JSON المُصدَّر من إضافة الكوكيز</label>
<textarea id="tokinput" rows="2" dir="ltr" placeholder="sessionid=...; sid_tt=..."></textarea>
<div class="row" style="margin-top:8px">
<button id="tokvalidate" class="secondary" type="button">فحص التوكن</button>
<button id="toksave" type="button">حفظ لكل الحسابات</button>
<input id="tokuser" dir="ltr" placeholder="اختبار حساب">
<button id="tokcheck" class="secondary" type="button">اختبر الفتح</button>
</div>
<p id="tokresult" hidden></p>
<div id="tokstate"></div>
</details>
</section>
<section class="panel" id="youtube-panel" hidden>
<h2>YouTube</h2>
<div class="row">
<button id="ytcheck" class="secondary" type="button">فحص OAuth والقناة</button>
<button id="ytadopt" class="secondary" type="button" hidden>اعتماد القناة الجديدة فقط</button>
<button id="ytstart" type="button">تفعيل البث التلقائي</button>
<button id="ytstop" class="danger" type="button">إيقاف وإنهاء البث الجاري</button>
</div>
<p id="ytchannelnotice" role="status" aria-live="polite" hidden></p>
<div class="kv" style="margin:14px 0">
<b>الاتصال</b><span id="ytconn">—</span>
<b>حالة البث</b><span id="ytstate">—</span>
<b>العنوان الحالي</b><span id="yttitle">—</span>
<b>الجلسة</b><span id="ytsession">—</span>
<b>Relay</b><span id="ytrelay">—</span>
</div>
<div id="events" class="cards"></div>
<div class="row">
<select id="sendmode">
<option value="auto">auto — نسخ H264 إن ثبت، وإلا تحويل خفيف 720</option>
<option value="copy">copy — بدون إعادة ترميز</option>
<option value="fixed720">fixed720 — كانفس ثابت (CPU أعلى)</option>
<option value="fixed1080">fixed1080 — كانفس 1080</option>
</select>
<button class="secondary" id="sendmodeset" type="button">تطبيق الوضع</button>
</div>
<details><summary>قوالب العنوان والوصف</summary>
<form id="settingsform">
<label>الخصوصية</label>
<select id="privacy"><option value="public">عام</option><option value="unlisted">غير مُدرج</option><option value="private">خاص</option></select>
<label>قالب العنوان — الافتراضي: {broadcaster} | {date} {time}</label>
<input id="titletemplate">
<label>قالب الوصف</label>
<textarea id="desctemplate" rows="4"></textarea>
<p><small>المتغيرات: {broadcaster} {name} {title} {username} {date} {time} {hashtags} {room_id} — التاريخ والوقت بتوقيت بغداد لحظة بدء جلسة تيك توك.</small></p>
<button type="submit" id="savesettings">حفظ</button>
</form>
</details>
</section>
<section class="panel" id="system-panel" hidden>
<h2>System</h2>
<div class="kv">
<b>Monitoring</b><span id="mon">—</span>
<b>الحسابات</b><span id="nacc">—</span>
<b>Relay active</b><span id="nrel">—</span>
<b>CPU</b><span id="ncpu">—</span>
<b>RAM</b><span id="nram">—</span>
<b>آخر خطأ</b><span id="lasterr">لا يوجد</span>
</div>
<h2 style="margin-top:16px">السجل الحي</h2>
<div id="logs"></div>
</section>
<footer>لا تسجيل محلي ولا أرشيف فيديو. المسار: TikTok → الذاكرة/الأنبوب → FFmpeg → YouTube RTMPS. أسرار OAuth والتوكنات لا تظهر هنا.</footer>
</main>
<script>
const $=id=>document.getElementById(id),esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
// An OAuth check is the operator's candidate, not the old journal's active channel.
// Periodic refresh must NEVER overwrite it with y.channel from the previous channel.
let settingsLoaded=false,checkedChannel=null,lastY=null,polling=false,pollingSince=0,youtubeBusy=false;
let authHeader='';try{authHeader=sessionStorage.getItem('on-basic-auth')||''}catch{}
function error(text){$('error').textContent=text;$('error').hidden=!text}
function showAuth(ok){
$('login-panel').hidden=ok;$('accounts-panel').hidden=!ok;$('youtube-panel').hidden=!ok;$('system-panel').hidden=!ok;$('logout').hidden=!ok;
if(!ok){authHeader='';checkedChannel=null;lastY=null;settingsLoaded=false;renderCheckedChannel();try{sessionStorage.removeItem('on-basic-auth')}catch{};$('admin-password').focus()}
}
function basicHeader(password){
const bytes=new TextEncoder().encode('admin:'+password);let raw='';for(const b of bytes)raw+=String.fromCharCode(b);return 'Basic '+btoa(raw)
}
async function api(url,method='GET',body){
const headers={};if(body)headers['Content-Type']='application/json';if(authHeader)headers.Authorization=authHeader;
const r=await fetch(url,{method,cache:'no-store',headers,body:body?JSON.stringify(body):undefined});
let j;try{j=await r.json()}catch{}
if(r.status===401){showAuth(false);throw Error('كلمة السر غير صحيحة أو انتهت جلسة الصفحة')}
if(!r.ok)throw Error(typeof j?.detail==='string'?j.detail:'تعذر تنفيذ الطلب');return j;
}
function fillSettings(s){$('privacy').value=s.privacy||'public';$('titletemplate').value=s.title||'';$('desctemplate').value=s.description||'';settingsLoaded=true}
function channelSwitchPending(){return !!(checkedChannel?.previous&&checkedChannel.previous.id!==checkedChannel.id)}
function renderCheckedChannel(){
const pending=channelSwitchPending(),notice=$('ytchannelnotice');
$('ytadopt').hidden=!pending;notice.hidden=!checkedChannel;
notice.textContent=!checkedChannel?'':pending
? 'القناة الجديدة بعد الفحص: «'+checkedChannel.title+'» ('+checkedChannel.id+') بدل «'+checkedChannel.previous.title+'». لم تُعتمد بعد؛ يمكنك اعتمادها فقط دون تشغيل بث. أحداث القناة السابقة تُؤرشف محليًا ولا تُعدَّل على يوتيوب.'
: 'القناة المؤكدة بالفحص: «'+checkedChannel.title+'» ('+checkedChannel.id+').';
}
function updateYouTubeControls(){
const y=lastY||{};
$('ytcheck').disabled=youtubeBusy;$('ytadopt').disabled=youtubeBusy;
$('ytstart').disabled=youtubeBusy||!!y.automatic;
$('ytstop').disabled=youtubeBusy||(!y.automatic&&!(y.events||[]).length);
}
async function checkYouTubeChannel(){
const j=await api('/api/youtube/check','POST');
if(!j.channel?.id)throw Error('لم يُرجع الفحص قناة مفوضة');
checkedChannel=j.channel;renderCheckedChannel();return checkedChannel;
}
function channelConfirmation(channel,start){
const previous=channel.previous&&channel.previous.id!==channel.id?channel.previous:null;
let text=previous?'تبديل القناة من «'+previous.title+'» إلى «'+channel.title+'» ('+channel.id+')؟\nستتوقف مرسلات القناة السابقة وتُؤرشف أحداثها محليًا فقط؛ لن تُحذف أو تُعدَّل على يوتيوب.\n'
:'القناة: «'+channel.title+'» ('+channel.id+').\n';
return text+(start?'تفعيل Relay التلقائي وإنشاء بث عند رصد LIVE على هذه القناة؟':'اعتماد القناة فقط، مع إبقاء الأتمتة معطلة عند التبديل؟');
}
function stateLabel(st){
const m={OFFLINE:'Offline',CHECKING:'فحص',LIVE_DETECTED:'اكتشاف LIVE',STARTING:'بدء',LIVE:'LIVE',STALLED:'توقّف المصدر',RECONNECTING:'إعادة اتصال',STOPPING:'إيقاف',ERROR:'خطأ',BACKOFF:'انتظار'};
return m[st]||st||'—';
}
function fmtDur(s){s=Math.max(0,s|0);const h=Math.floor(s/3600),m=Math.floor((s%3600)/60);return h?h+'س '+m+'د':m+'د '+ (s%60)+'ث'}
async function refresh(){
if(!authHeader||document.hidden)return;
if(polling&&Date.now()-(pollingSince||0)<30000)return;
polling=true;pollingSince=Date.now();
try{
const [ry,rs]=await Promise.all([
api('/api/youtube').then(v=>({v})).catch(e=>({e})),
api('/api/state').then(v=>({v})).catch(e=>({e}))
]);
const y=ry.v||{events:[],settings:null,channel:null};
const s0=rs.v||{tasks:[],logs:[]};
const ytError=ry.e?String(ry.e.message||ry.e):'', stateError=rs.e?String(rs.e.message||rs.e):'';
if(ry.v)lastY=y;
const evs=y.events||[], tasks=s0.tasks||[];
const notes=[]; if(ytError)notes.push(ytError); if(stateError)notes.push(stateError); error(notes.join('\n'));
$('tasks').innerHTML=tasks.length?tasks.map(x=>{
const live=x.state==='LIVE';
return `<article class="card"><h3>@${esc(x.username)}</h3>
<p><span class="badge ${live?'live':(x.state==='OFFLINE'?'off':'')}">${esc(stateLabel(x.state))}</span>
${x.nickname?' · '+esc(x.nickname):''}</p>
<p>LIVE منذ: ${live?esc(fmtDur(x.live_for||0)):'—'}</p>
<p>المصدر: ${esc(x.source||'—')}</p>
<p>Codec: ${esc(x.codec||'—')} · ${esc(x.quality||'')} · room ${esc(x.room_id||'—')}</p>
<p>PID: ${x.relay_pid||'—'} · بايت: ${esc(x.bytes_received||0)} · إعادة: ${esc(x.restarts||0)}</p>
<p>آخر بايت: ${x.source_age==null||x.source_age===''?'—':(Number(x.source_age).toFixed(1)+'ث')}</p>
<p>YouTube: ${esc(x.youtube_status||'—')} · health ${esc(x.youtube_health||'—')}${x.relay_alive?' · Relay':''}</p>
<small>${esc(x.status||'')}${x.last_error?' · '+esc(x.last_error):''}</small>
<p>توكن: ${x.has_token?'موجود ✓':'غير مضاف'}</p>
<div class="row">
<button data-user="${esc(x.username)}" data-action="start">تشغيل</button>
<button class="secondary" data-user="${esc(x.username)}" data-action="stop">إيقاف</button>
<button class="secondary" data-user="${esc(x.username)}" data-action="token">توكن</button>
<button class="danger" data-user="${esc(x.username)}" data-action="delete">حذف</button>
</div></article>`;
}).join(''):'<p>أضف حسابات للمراقبة.</p>';
const ev=evs[0]||null;
const conn=[];
if(y.oauth_configured||y.configured) conn.push(y.channel?('OAuth: '+y.channel.title):'OAuth موجود');
else conn.push('OAuth غير مفعّل');
if(y.rtmp_configured||s0.rtmp_configured) conn.push('RTMPS env جاهز');
else conn.push('لا YOUTUBE_RTMP_URL/KEY');
if(!(y.metadata_automation||s0.metadata_automation)) conn.push('تحديث العنوان غير متاح بدون OAuth — الفيديو لا يتوقف');
$('ytconn').textContent=conn.join(' · ');
$('ytstate').textContent=y.automatic?(y.status||'أتمتة OAuth مفعّلة'):(s0.rtmp_configured?'Relay عبر RTMPS بدون OAuth':'معطّل');
const liveTask=tasks.find(t=>t.relay_alive||t.state==='LIVE');
$('yttitle').textContent=ev?(ev.title||'—'):(liveTask&&liveTask.session_title)||'لا بث جارٍ';
$('ytsession').textContent=ev?(ev.username+' · '+(ev.youtube_status||ev.phase||'')):(liveTask?liveTask.username:'—');
$('ytrelay').textContent=String(s0.active_relays||evs.length||0)+' نشط';
$('events').innerHTML=evs.length?evs.map(e=>`<article class="card"><h3>${esc(e.title||'بث')}</h3>
<p>@${esc(e.username)} · ${esc(e.phase||'')} · YT ${esc(e.youtube_status||'—')}</p>
<p>المصدر: ${esc(e.source||'—')}</p>
<p>وضع الإرسال الفعلي: ${esc(e.send_mode||'قيد التحديد')}</p>
${e.pending_send_mode?`<p>الترميز الثابت ينتظر خانة: ${esc(e.pending_send_mode)}</p>`:''}
<small>${esc(e.status||e.note||'')} · ${Math.floor(e.output_seconds||0)} ث</small>
${e.broadcast_id?`<p><a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=${encodeURIComponent(e.broadcast_id)}">فتح على يوتيوب</a></p>`:''}
</article>`).join(''):'<p>عند LIVE يُفتح Relay عبر RTMPS (مفتاح البيئة أو OAuth). فشل تحديث العنوان لا يوقف الفيديو.</p>';
const lastErr=tasks.map(t=>t.last_error).filter(Boolean)[0] || (s0.logs||[]).filter(l=>/error|فشل|stall|خطأ/i.test(l.message||'')).slice(-1)[0]?.message || 'لا يوجد';
$('mon').textContent=s0.monitor_running?'يعمل':'متوقف';
$('nacc').textContent=String(tasks.length);
$('nrel').textContent=String(s0.active_relays||evs.length||0)+' / '+(Number(s0.max_concurrent_relays)===0?'∞':(s0.max_concurrent_relays||'?'));
$('ncpu').textContent=(s0.cpu_pct_of_quota!=null
? (s0.cpu_pct_of_quota+'% من '+(s0.quota_cpus||'?')+' vCPU')
: (s0.load1!=null?('load '+s0.load1):'—'));
$('nram').textContent=(s0.mem_avail_mb!=null?(s0.mem_avail_mb+' MB متاح / '+s0.mem_total_mb+' MB'):'—');
$('lasterr').textContent=lastErr;
$('sysbadge').textContent=s0.monitor_running?'Monitor · ON':'Monitor · OFF';
$('updated').textContent='آخر تحديث '+new Date().toLocaleTimeString('ar-IQ')+' · فحص كل '+(s0.check_interval||'?')+'ث · stall '+(s0.source_stall_timeout||'?')+'ث';
const cur=y.send_mode_setting||y.send_mode||'auto';
const sel=$('sendmode'); if(document.activeElement!==sel) sel.value=['auto','fixed720','fixed1080','copy'].includes(cur)?cur:'auto';
updateYouTubeControls();renderCheckedChannel();
$('savesettings').disabled=!!y.automatic||!!evs.length;
if(!settingsLoaded&&y.settings)fillSettings(y.settings);
$('logs').innerHTML=(s0.logs||[]).slice().reverse().map(l=>`<div>${esc(l.time||'')} · @${esc(l.user||'')} · ${esc(l.message||'')}</div>`).join('')||'<p>لا أحداث بعد.</p>';
renderTiktok(s0.tiktok);
} finally {polling=false}
}
function renderTiktok(t){if(!t){$('tokstate').innerHTML='';return}
const accts=Object.entries(t.accounts||{});
let rows=`<small>توكن عام: ${t.default_authenticated?esc(t.default_masked):'غير مضبوط'}</small>`;
rows+=accts.map(([u,v])=>`<div>@${esc(u)} — ${v.authenticated?esc(v.masked):'يعتمد على العام'}</div>`).join('');
$('tokstate').innerHTML=rows}
function tokShow(html){const e=$('tokresult');e.innerHTML=html;e.hidden=false}
$('login').onsubmit=async e=>{e.preventDefault();const p=$('admin-password').value;const msg=$('login-error');msg.hidden=true;
authHeader=basicHeader(p);try{await api('/api/state');try{sessionStorage.setItem('on-basic-auth',authHeader)}catch{};$('admin-password').value='';showAuth(true);await refresh()}
catch(err){showAuth(false);msg.textContent='تعذر الدخول: '+String(err.message||err);msg.hidden=false}};
$('logout').onclick=()=>{showAuth(false);error('');$('login-error').hidden=true};
$('tokvalidate').onclick=async()=>{const raw=$('tokinput').value.trim();if(!raw){tokShow('الصق التوكن أولًا');return}
try{$('tokvalidate').disabled=true;const r=await api('/api/tiktok/token/validate','POST',{token:raw});tokShow(esc(r.message||''));if(r.ok)await refresh()}catch(e){tokShow(esc(e.message))}finally{$('tokvalidate').disabled=false}};
$('toksave').onclick=async()=>{const raw=$('tokinput').value.trim();if(!raw){tokShow('الصق التوكن أولًا');return}
try{$('toksave').disabled=true;const r=await api('/api/tiktok/token/validate','POST',{token:raw});tokShow(esc(r.message||''));if(r.ok){$('tokinput').value='';await refresh()}}catch(e){tokShow(esc(e.message))}finally{$('toksave').disabled=false}};
$('tokcheck').onclick=async()=>{const u=$('tokuser').value.trim();if(!u){tokShow('اكتب اسم الحساب');return}
try{$('tokcheck').disabled=true;const r=await api('/api/tiktok/token/check/'+encodeURIComponent(u),'POST');tokShow(esc(r.message||r.result||''))}catch(e){tokShow(esc(e.message))}finally{$('tokcheck').disabled=false}};
$('sendmodeset').onclick=async()=>{try{$('sendmodeset').disabled=true;await api('/api/youtube/settings','POST',{send_mode:$('sendmode').value});await refresh()}catch(e){error(e.message)}finally{$('sendmodeset').disabled=false}};
$('ytcheck').onclick=async()=>{
if(youtubeBusy)return;youtubeBusy=true;updateYouTubeControls();
try{const channel=await checkYouTubeChannel();await refresh();alert('القناة المفوضة: '+channel.title+(channelSwitchPending()?' — بانتظار تأكيد اعتمادها':''))}
catch(e){error(e.message)}finally{youtubeBusy=false;updateYouTubeControls()}
};
$('ytadopt').onclick=async()=>{
if(youtubeBusy)return;youtubeBusy=true;updateYouTubeControls();
try{
const channel=await checkYouTubeChannel();
if(!confirm(channelConfirmation(channel,false)))return;
const j=await api('/api/youtube/channel','POST',{channel_id:channel.id});
checkedChannel=j.channel;renderCheckedChannel();await refresh();
alert('تم اعتماد القناة «'+j.channel.title+'». اعتماد القناة وحده لا يبدأ أي بث.');
}catch(e){error(e.message)}finally{youtubeBusy=false;updateYouTubeControls()}
};
$('ytstart').onclick=async()=>{
if(youtubeBusy)return;youtubeBusy=true;updateYouTubeControls();
try{
// Recheck even after a previous check: OAuth Secrets may have changed in another tab.
const channel=await checkYouTubeChannel();
if(!confirm(channelConfirmation(channel,true)))return;
await api('/api/youtube/start','POST',{channel_id:channel.id});
checkedChannel={id:channel.id,title:channel.title};renderCheckedChannel();await refresh();
}catch(e){error(e.message)}finally{youtubeBusy=false;updateYouTubeControls()}
};
$('ytstop').onclick=async()=>{
if(youtubeBusy||!confirm('إيقاف الأتمتة وإنهاء البث الجاري؟'))return;
youtubeBusy=true;updateYouTubeControls();
try{await api('/api/youtube/stop','POST');await refresh()}catch(e){error(e.message)}finally{youtubeBusy=false;updateYouTubeControls()}
};
$('settingsform').onsubmit=async e=>{e.preventDefault();try{await api('/api/youtube/settings','POST',{name:'',privacy:$('privacy').value,title:$('titletemplate').value,description:$('desctemplate').value});settingsLoaded=false;await refresh()}catch(e){error(e.message)}};
$('add').onsubmit=async e=>{e.preventDefault();try{await api('/api/accounts','POST',{username:$('username').value,token:$('token').value});$('username').value='';$('token').value='';await refresh()}catch(e){error(e.message)}};
document.addEventListener('click',async e=>{const b=e.target.closest('button');if(!b)return;try{
if(b.dataset.action==='token'){const t=prompt('توكن جلسة تيك توك للحساب @'+b.dataset.user+' (فارغ للحذف):');if(t===null)return;await api('/api/accounts/'+encodeURIComponent(b.dataset.user)+'/token','POST',{token:t});await refresh();return}
if(b.dataset.action){if(b.dataset.action==='delete'&&!confirm('حذف الحساب؟'))return;await api('/api/accounts/'+encodeURIComponent(b.dataset.user)+'/'+b.dataset.action,'POST');await refresh()}
}catch(e){error(e.message)}});
$('reload').onclick=()=>refresh();
document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh()});
showAuth(!!authHeader);if(authHeader)refresh();setInterval(refresh,10000);
</script></body></html>
|