Spaces:
Running
Running
Download index.html from Reoo88/Prayer-Time-Bot: direct link, hf CLI and curl.
- Browser
- Download file 22.6 kB
-
https://huggingface.co/spaces/Reoo88/Prayer-Time-Bot/resolve/main/index.html
- Command line
-
hf download hf://spaces/Reoo88/Prayer-Time-Bot/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Reoo88/Prayer-Time-Bot/resolve/main/index.html
22.6 kB
| <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> | |