Hode / index.html
Reooiq's picture
Replace Hode with Reoo88/Tktktk main (00967c2) exactly; PASSWORD=admin for login
56bfa33
Raw History Blame Contribute Delete
20.1 kB
<!doctype html><html lang="ar" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>قصي • مدير البثوث</title><style>
:root{color-scheme:dark;font-family:system-ui}body{margin:0;background:#0c1220;color:#e4eaf7}main{max-width:1100px;margin:40px auto;padding:0 20px}header{display:flex;justify-content:space-between;align-items:center}h1{font-size:32px;margin:0}p{color:#9daec8;line-height:1.8}small{color:#9daec8}.badge{border:1px solid #355445;border-radius:30px;padding:8px 15px;color:#78e1b7}.panel{background:#151e30;border:1px solid #28364f;padding:24px;border-radius:18px;margin:22px 0}input,button{font:inherit;border-radius:10px;padding:12px;border:1px solid #364864}input{background:#0c1220;color:white;flex:1;min-width:100px}button{background:#70e1b4;color:#10251d;cursor:pointer;font-weight:650}button.secondary{background:#23324b;color:#d4e0f5}button.danger{background:#452735;color:#ffbccb}form{display:flex;gap:12px;flex-wrap:wrap}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}.card{border:1px solid #32405a;border-radius:12px;padding:18px}.card h3{direction:ltr;text-align:right}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.file{padding:14px 0;border-bottom:1px solid #29364c;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}a{color:#79dcc5}#logs{max-height:280px;overflow:auto;font:14px/1.9 monospace}#error{color:#ffc1c1}h2{font-size:20px}footer{color:#8e9eb6;font-size:13px;margin:30px 0}code{overflow-wrap:anywhere}button:disabled{opacity:.5}
textarea,select{font:inherit;background:#0c1220;color:#e4eaf7;border:1px solid #364864;border-radius:10px;padding:12px;box-sizing:border-box;width:100%}label{display:block;margin:14px 0 6px;color:#becce2}.settings{display:grid;grid-template-columns:1fr 1fr;gap:18px}.settings input{width:100%;box-sizing:border-box}.wide{grid-column:1/-1}.notice{border-right:3px solid #70e1b4;padding:10px 15px;background:#142b2b}#ytstate{white-space:pre-line}button:disabled{cursor:wait} @media(max-width:650px){.settings{grid-template-columns:1fr}header{gap:10px}.wide{grid-column:auto}}
</style></head><body><main>
<header><div><small>V9 • بث متزامن لكل مذيع • إصلاح تغيّر أبعاد الصورة • توكن تيك توك للبثوث المقيدة عمريًا</small><h1>قصي مهاجر عراقي</h1></div><span class="badge">مرصد البثوث</span></header>
<p>تسجيل أصلي مستقل لكل حساب • حدث يوتيوب مستقل لكل مذيع مباشر يبث بنفس الوقت • اسم المذيع وعنوان البث والهاشتاكات تُجلب تلقائيًا من حسابه</p>
<div id="error" role="alert" class="notice" hidden></div>
<section class="panel"><h2>الحسابات التابعة للمذيع</h2>
<form id="add"><input id="username" placeholder="اسم المستخدم أو رابط الحساب الكامل" required><input id="token" placeholder="توكن الجلسة sessionid — اختياري، للبثوث المقيدة عمريًا"><button>إضافة ومراقبة</button></form>
<p id="capacity"></p><div id="tasks" class="cards"></div>
<p class="notice">التوكن هو كوكيز جلسة تيك توك لحساب بالغ، يُستخدم فقط لفتح البثوث المقيدة بفئة عمرية. يُخزن في مجلد البيانات ولا يظهر في الواجهة أو السجلات. استخدم حسابًا مخصصًا له.</p></section>
<section class="panel"><h2>توكن تيك توك — للبثوث المقيدة عمريًا (18+)</h2>
<p>تيك توك يخفي روابط الفيديو عن الزائر غير المسجّل في البثوث المقيّدة عمريًا، فلا يمكن رصدها ولا تسجيلها. التوكن يفتحها.</p>
<label for="tokinput">الصق <b>كامل سطر Cookie</b> (أو قيمة <code dir="ltr">sessionid</code> وحدها)</label>
<textarea id="tokinput" rows="3" dir="ltr" placeholder="sessionid=...; sessionid_ss=...; sid_tt=...; tt-target-idc=useast1a; sid_guard=..."></textarea>
<div class="row" style="margin-top:12px">
<button id="tokvalidate" class="secondary">١. فحص التوكن عند تيك توك</button>
<button id="toksave">٢. فحصه وحفظه لكل الحسابات</button>
</div>
<div class="row" style="margin-top:10px">
<input id="tokuser" dir="ltr" placeholder="اسم حساب مقيد عمريًا (مثال: user.name)">
<button id="tokcheck" class="secondary">٣. اختبر فتح الحساب</button>
</div>
<p id="tokresult" class="notice" hidden></p>
<div id="tokstate" style="margin-top:14px"></div>
<p class="notice"><b>مهم:</b> جلسات تيك توك قصيرة العمر. فحصنا على ملف كوكيز حقيقي أظهر <code dir="ltr">sid_guard</code> بعمر <b>6 ساعات</b> فقط، وبعدها يرفض تيك توك التوكن حتى لو كان "صحيحًا". لذلك الصق <b>كامل سطر Cookie</b> لا القيمة وحدها — حتى يرى التطبيق <code dir="ltr">sid_guard</code> ويعرض لك موعد الانتهاء ويحذّرك قبله. الحفظ من هذه اللوحة <b>لا يحتاج Factory reboot</b>.</p>
<p class="notice" style="border-right-color:#e1b470;background:#2b2414"><b>🚨 أمان:</b> لا تصدّر كوكيز المتصفح كاملة أبدًا. أداة التصدير تُخرج كل المواقع بما فيها <b>جوجل ويوتيوب</b> (<code dir="ltr">SID, HSID, APISID, SAPISID, __Secure-1PSID</code>…) وهذه تعادل كلمة مرورك. التطبيق يحتاج تيك توك فقط — انسخ <code dir="ltr">sessionid</code> وحدها، أو احذف كل سطر لا ينتهي بـ <code dir="ltr">tiktok.com</code>.</p>
</section>
<section class="panel"><h2>مدير أحداث يوتيوب — بث متزامن</h2>
<p>يفحص الحسابات المفعّلة، وكل مذيع يفتح بثًا مباشرًا يصير له حدث يوتيوب مستقل باسمه وعنوانه، وتبقى الأحداث كلها تعمل بنفس الوقت حتى تنتهي جلساتها. جلسة جديدة = حدث جديد. لا يرسل شاشة انتظار.</p>
<div class="row"><button id="ytcheck" class="secondary">١. فحص OAuth والقناة</button><button id="ytstart">٢. تفعيل الإدارة التلقائية</button><button id="ytstop" class="danger">تعطيل وإنهاء الأحداث الجارية</button></div>
<p id="channel">لم يتم فحص القناة بعد.</p><p id="ytstate">تحميل الحالة…</p>
<div id="events" class="cards"></div>
<small id="budget"></small>
<p class="notice">التفعيل يسمح للتطبيق بإنشاء أحداث متزامنة (حتى الحد الأقصى المحدد)، وبدئها وإنهائها، وحذف أحداثه الفارغة فقط. لا ينظف أحداث النسخ القديمة. إعداد التفعيل محفوظ ويُستأنف بعد إعادة التشغيل. لا ضمان تشغيل 24/7. كل حدث إضافي يستهلك معالجًا وإنترنت؛ راقب أداء الاستضافة.</p>
<details><summary>قوالب العنوان والوصف — تُطبّق على الأحداث الجديدة</summary>
<form id="settingsform" style="display:block"><div class="settings">
<div><label for="privacy">خصوصية الأحداث</label><select id="privacy"><option value="public">عام</option><option value="unlisted">غير مُدرج</option><option value="private">خاص</option></select></div>
<div class="wide"><label for="titletemplate">قالب العنوان</label><input id="titletemplate" required></div>
<div class="wide"><label for="desctemplate">قالب الوصف</label><textarea id="desctemplate" rows="6"></textarea></div></div>
<p>الاسم والعنوان والهاشتاكات تُجلب تلقائيًا من حساب تيك توك المبثوث عند بدء الجلسة: <code dir="ltr">{name}</code> اسم المذيع من حسابه، <code dir="ltr">{title}</code> عنوان بثه على تيك توك، <code dir="ltr">{hashtags}</code> هاشتاكات بثه. متغيرات إضافية: <code dir="ltr">{username} {date} {time} {room_id}</code>. يُضاف رابط حساب المذيع نفسه تلقائيًا لكل حدث. العنوان الناتج حتى 100 حرف. تعديل القوالب يتطلب تعطيل الإدارة وانتظار إغلاق الأحداث.</p>
<button type="submit" id="savesettings">حفظ إعدادات الأحداث</button></form></details>
</section>
<section class="panel"><h2>التسجيلات المحفوظة</h2><small>تظهر بعد اكتمال تجهيز الملف. HEVC قد يحتاج VLC. لا تقسيم زمني افتراضيًا؛ قد تظهر ملفات منفصلة عند الانقطاع.</small><div id="files"></div></section>
<section class="panel"><h2>سجل التشغيل</h2><div id="logs"></div></section>
<footer>أسرار OAuth وتوكنات الجلسات تبقى في Secrets ومجلد البيانات فقط، ولا تظهر في الواجهة. راقب حصة التخزين من Hugging Face. تقدير API محلي وليس قراءة لحصة Google الفعلية. استخدمه للمحتوى الذي تملك حق إعادة نشره.</footer>
</main><script>
const $=id=>document.getElementById(id),esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
let settingsLoaded=false,channelId='',lastY=null,polling=false;
function error(text){$('error').textContent=text;$('error').hidden=!text}
async function api(url,method='GET',body){const r=await fetch(url,{method,cache:'no-store',headers:body?{'Content-Type':'application/json'}:{},body:body?JSON.stringify(body):undefined});let j;try{j=await r.json()}catch{}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}
async function refresh(){if(polling)return;polling=true;try{
const [y,s]=await Promise.all([api('/api/youtube'),api('/api/state')]);lastY=y;
const evs=y.events||[];
$('capacity').textContent=`${s.version} • حد الحسابات: ${s.max_accounts} • أقصى أحداث يوتيوب متزامنة: ${y.max_events||3}`;
$('channel').textContent=y.channel?`القناة المرتبطة: ${y.channel.title} · ${y.channel.id}`:'اضغط فحص OAuth للتأكد من القناة قبل النشر.';
channelId=y.channel?.id||'';
$('ytstate').textContent=`${y.automatic?'الإدارة التلقائية مفعّلة':'الإدارة التلقائية معطّلة'} · وضع المصدر: ${y.video_mode||'original'} · وضع الإرسال: ${y.send_mode||'copy'} · أحداث جارية: ${evs.length}\n${y.status}`;
$('events').innerHTML=evs.length?evs.map(e=>`<article class="card"><h3>${esc(e.title||'حدث بث')}</h3><p>@${esc(e.username)}${e.nickname?' · '+esc(e.nickname):''}</p><small>المرحلة: ${esc(e.phase||'—')} · حالة يوتيوب: ${esc(e.youtube_status||'—')}${e.closing?' · جارٍ الإنهاء':''}</small><p>${esc(e.status||e.note||'—')}</p><small>${e.source?esc('مصدر: '+e.source+' · '):''}${Math.floor(e.output_seconds||0)} ثانية إرسال</small>${e.broadcast_id?`<p><a target="_blank" rel="noopener noreferrer" href="https://www.youtube.com/watch?v=${encodeURIComponent(e.broadcast_id)}">فتح الحدث على يوتيوب</a></p>`:''}</article>`).join(''):'<p>لا توجد أحداث جارية. عند بدء أي مذيع مباشرة يُفتح حدث مستقل باسمه وعنوانه من حسابه.</p>';
$('budget').textContent=`استهلاك API محلي تقديري: ${y.budget?.estimated_units||0} وحدة • جلسات في سجل التطبيق: ${y.history_count||0} • ${y.configured?'أسرار OAuth موجودة (الفحص يؤكد صلاحيتها)':'أسرار OAuth غير مكتملة'}`;
$('ytstart').disabled=!!y.automatic;$('ytstop').disabled=!y.automatic&&!evs.length;
$('savesettings').disabled=!!y.automatic||!!evs.length;
if(!settingsLoaded&&y.settings)fillSettings(y.settings);
$('tasks').innerHTML=s.tasks.length?s.tasks.map(t=>`<article class="card"><h3>@${esc(t.username)}</h3><p>${esc(t.status)}</p><small>${esc(t.quality||'الجودة لم تحدد بعد')} · ${t.received_mb||0} MB · ${Math.floor((t.elapsed_seconds||0)/60)} دقيقة</small><p>${t.enabled?'المراقبة مفعّلة':'المراقبة معطّلة'} · توكن الجلسة: ${t.has_token?'موجود ✓':'غير مضاف'}</p><div class="row"><button data-user="${esc(t.username)}" data-action="start">تشغيل</button><button class="secondary" data-user="${esc(t.username)}" data-action="stop">إيقاف</button><button class="secondary" data-user="${esc(t.username)}" data-action="token">توكن</button><button class="danger" data-user="${esc(t.username)}" data-action="delete">حذف</button></div></article>`).join(''):'<p>أضف حسابات المذيع المطلوبة فقط.</p>';
$('files').innerHTML=s.files.length?s.files.map(f=>`<div class="file"><div><code>${esc(f.name)}</code><br><small>${f.mb} MB</small></div><div class="row"><a href="/api/files/${encodeURIComponent(f.name)}">تنزيل</a><button class="danger" data-file="${esc(f.name)}">حذف الملف</button></div></div>`).join(''):'<p>لا توجد ملفات مكتملة بعد.</p>';
$('logs').innerHTML=s.logs.slice().reverse().map(l=>`<div>${esc(l.time)} · @${esc(l.user)} · ${esc(l.message)}</div>`).join('');
renderTiktok(s.tiktok);
}catch(e){error(e.message)}finally{polling=false}}
function expiryNote(e){if(!e)return '';
if(e.expired)return `<br><small style="color:#ffc1c1">⚠ انتهت صلاحيته ${esc(e.expires_text)} — حدّثه</small>`;
const h=Math.floor(e.seconds_left/3600),m=Math.floor((e.seconds_left%3600)/60);
const warn=e.seconds_left<3600;
return `<br><small${warn?' style="color:#ffd9a0"':''}>${warn?'⚠ ':''}ينتهي بعد ${h}س ${m}د (${esc(e.expires_text)})</small>`}
function renderTiktok(t){if(!t){$('tokstate').innerHTML='';return}
const accts=Object.entries(t.accounts||{});
let rows=`<div class="file"><div>توكن عام (كل الحسابات)</div><div>${t.default_authenticated?`<span class="badge">مفعّل</span> ${esc(t.default_masked)} <small>· ${esc(t.default_source||'')}</small>${expiryNote(t.default_expiry)}`:'<small>غير مضبوط</small>'}</div></div>`;
rows+=accts.length?accts.map(([u,v])=>`<div class="file"><div>@${esc(u)}</div><div>${v.authenticated?`<span class="badge">له توكن</span> ${esc(v.masked)} <small>· ${esc(v.source||'')}${v.tt_target_idc?' · '+esc(v.tt_target_idc):''}</small>${expiryNote(v.expiry)}`:'<small>يعتمد على التوكن العام</small>'}</div></div>`).join(''):'';
$('tokstate').innerHTML=rows}
function tokShow(html){const e=$('tokresult');e.innerHTML=html;e.hidden=false}
$('tokvalidate').onclick=async()=>{const raw=$('tokinput').value.trim();if(!raw){tokShow('✖ الصق التوكن أولًا');return}
try{error('');$('tokvalidate').disabled=true;tokShow('… يسأل تيك توك');
const r=await api('/api/tiktok/token/validate','POST',{token:raw});
tokShow(`<b>${r.ok?'✔ ':'✖ '}${esc(r.message||'')}</b>`+(r.checks?.length?`<br><small>${r.checks.map(esc).join('<br>')}</small>`:'')+(r.account?`<br><small dir="ltr">${esc(JSON.stringify(r.account))}</small>`:''));
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{error('');$('toksave').disabled=true;tokShow('… فحص ثم حفظ');
const r=await api('/api/tiktok/token/validate','POST',{token:raw});
if(!r.ok){tokShow(`✖ لم يُحفظ — ${esc(r.message||'')}`+(r.checks?.length?`<br><small>${r.checks.map(esc).join('<br>')}</small>`:''));return}
tokShow(`<b>✔ حُفظ ويعمل</b><br><small>${esc(r.message||'')} · ${esc(r.sessionid_masked||'')}</small>`);
$('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{error('');$('tokcheck').disabled=true;tokShow('… يختبر فتح الحساب عند تيك توك');
const r=await api('/api/tiktok/token/check/'+encodeURIComponent(u),'POST');
tokShow(`<b>${r.ok===false?'✖ ':'✔ '}${esc(r.message||r.result||'')}</b><br><small dir="ltr">result=${esc(r.result||'')} · anonymous=${r.anonymous?.urls??'—'} urls · authenticated=${r.authenticated?.urls??'—'} urls · token_configured=${r.token_configured}</small>`)}catch(e){tokShow('✖ '+esc(e.message))}finally{$('tokcheck').disabled=false}};
$('ytcheck').onclick=async()=>{try{error('');$('ytcheck').disabled=true;const j=await api('/api/youtube/check','POST');channelId=j.channel.id;await refresh();alert(`تم تفويض القناة: ${j.channel.title}\n${j.channel.id}\nلم يُنشأ أو يُنشر أي حدث بهذا الفحص.`)}catch(e){error(e.message)}finally{$('ytcheck').disabled=false}};
$('ytstart').onclick=async()=>{try{if(!channelId)throw Error('اضغط فحص OAuth أولًا');if(!confirm(`تفعيل إنشاء وبدء أحداث تلقائية لقناة ${lastY?.channel?.title||channelId}؟\nالخصوصية: ${lastY?.settings?.privacy}\nلكل مذيع مباشر حدث مستقل يبث بنفس الوقت (حتى ${lastY?.max_events||3} أحداث)، واسمه وعنوانه وهاشتاكاته تُجلب من حساب تيك توك نفسه.`))return;error('');await api('/api/youtube/start','POST',{channel_id:channelId});await refresh()}catch(e){error(e.message)}};
$('ytstop').onclick=async()=>{if(!confirm('تعطيل الإدارة وإنهاء جميع الأحداث الجارية مع الاحتفاظ بالأرشيف؟'))return;try{error('');await api('/api/youtube/stop','POST');await refresh()}catch(e){error(e.message)}};
$('settingsform').onsubmit=async e=>{e.preventDefault();try{error('');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{error('');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(`توكن جلسة تيك توك (sessionid) للحساب @${b.dataset.user} — يلزم للبثوث المقيدة بفئة عمرية.\nالصق قيمة sessionid أو كوكيز كاملة، أو اتركه فارغًا للحذف:`);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()}
if(b.dataset.file&&confirm('حذف التسجيل نهائيًا من التخزين؟')){await api('/api/files/'+encodeURIComponent(b.dataset.file),'DELETE');await refresh()}}catch(e){error(e.message)}});
document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh()});window.addEventListener('online',refresh);window.addEventListener('pageshow',refresh);refresh();setInterval(refresh,5000);
</script></body></html>