Files
thePR0M3TH3ANandClaude Opus 4.8 fdcf034464 fix(beacon): stop the processing spinner hanging forever (#28)
The torrent beacon's processing overlay was gated on WebTorrent's metadata-ready
callback (client.add/seed), which only fires once metadata arrives from peers. A
magnet with no live seeders never resolved it, so the spinner span forever, and a
torrent that emitted its own 'error' didn't always reach the client-level handler.

- beginProcessing() arms a 30s watchdog that drops the overlay and warns ("still
  searching for peers — it'll keep trying in the background") instead of hanging;
  the torrent stays in the table and keeps announcing.
- handleTorrentReady, the client error handler, and destroy() clear the watchdog.
- addMagnet/seedFiles wrap add/seed in try/catch and attach a per-torrent 'error'
  listener so bad/duplicate magnets fail visibly.
- Tests: tests/torrent/beacon-watchdog.test.mjs (JSDOM).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 21:57:11 -04:00

2 lines
26 KiB
JavaScript

import{IS_DEV_MODE as Be,IS_VERBOSE_DEV_MODE as Me,IS_LOCKDOWN_MODE as De,LOCKDOWN_TITLE as Ct,LOCKDOWN_MESSAGE as xt,ADMIN_SUPER_NPUB as It,ADMIN_DM_IMAGE_URL as vt,BITVID_WEBSITE_URL as Ot,BLOG_URL as Nt,COMMUNITY_URL as Bt,NOSTR_URL as Mt,GITHUB_URL as Dt,BETA_URL as Rt,DNS_URL as Ut,TIP_JAR_URL as Ht,MAX_WALLET_DEFAULT_ZAP as kt,PLATFORM_FEE_PERCENT as Ft,PLATFORM_LUD16_OVERRIDE as Pt,DEFAULT_RELAY_URLS_OVERRIDE as Vt,ADMIN_WHITELIST_MODE_STORAGE_KEY as Wt,DEFAULT_WHITELIST_MODE_ENABLED as qt,ALLOW_NSFW_CONTENT as jt,FEATURE_NIP71_INGEST as $t,DEFAULT_TRUST_SEED_NPUBS as Yt,DEFAULT_BLUR_THRESHOLD as Gt,DEFAULT_AUTOPLAY_BLOCK_THRESHOLD as Kt,DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as zt,DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as Xt,WATCH_HISTORY_KIND as Zt,WATCH_HISTORY_LIST_IDENTIFIER as Jt,WATCH_HISTORY_MAX_ITEMS as Qt,WATCH_HISTORY_BATCH_RESOLVE as er,WATCH_HISTORY_BATCH_PAGE_SIZE as tr,WATCH_HISTORY_PAYLOAD_MAX_BYTES as rr,WATCH_HISTORY_FETCH_EVENT_LIMIT as nr,WATCH_HISTORY_CACHE_TTL_MS as or,VIEW_COUNT_DEDUPE_WINDOW_SECONDS as ar,VIEW_COUNT_BACKFILL_MAX_DAYS as sr,VIEW_COUNT_CACHE_TTL_MS as ir,ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS as cr,DEFAULT_PLAYBACK_SOURCE as lr,DEFAULT_PLAYBACK_START_TIMEOUT as dr,ENABLE_NIP17_RELAY_WARNING as ur}from"../config/instance-config.js";var oe=typeof globalThis>"u"?null:globalThis,ae=e=>{if(typeof e=="boolean")return e;if(typeof e=="string"){let t=e.trim().toLowerCase();if(t==="true")return!0;if(t==="false")return!1}return null},re=ae(oe?.__BITVID_DEV_MODE_OVERRIDE__),ne=ae(oe?.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__),U=re===null?!!Be:re;var Re=ne===null?!!Me:ne;var Ue=!!De;typeof window<"u"&&(window.__BITVID_DEV_MODE__=U,window.__BITVID_VERBOSE_DEV_MODE__=Re,window.__BITVID_LOCKDOWN_MODE__=Ue);var k=()=>{};function He(){return typeof console>"u"||console===null?null:console}function ke(){let e=He();if(!e)return{log:k,info:k,debug:k,warn:k,error:k};let t=typeof e.log=="function"?e.log.bind(e):k;return{log:typeof e.log=="function"?e.log.bind(e):t,info:typeof e.info=="function"?e.info.bind(e):t,debug:typeof e.debug=="function"?e.debug.bind(e):t,warn:typeof e.warn=="function"?e.warn.bind(e):t,error:typeof e.error=="function"?e.error.bind(e):t}}var w=ke(),M="[bitvid]";function se(e){if(e.length===0)return{args:e,force:!1};let t=e[e.length-1];if(t&&typeof t=="object"&&!Array.isArray(t)&&Object.prototype.hasOwnProperty.call(t,"force")){let r=[...e],o=r.pop();return{args:r,force:!!o.force}}return{args:e,force:!1}}var A=Object.freeze({log:(...e)=>{U&&w.log(...e)},info:(...e)=>{U&&w.info(...e)},debug:(...e)=>{U&&w.debug(...e)},warn:(...e)=>{U&&w.warn(...e)},error:(...e)=>{U&&w.error(...e)}}),m=Object.freeze({log:(...e)=>{let{args:t,force:r}=se(e);if(!r&&t.length===0)return;if(r){w.log(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){w.log(`${M} ${o}`);return}o!==void 0&&w.log(M,o)},info:(...e)=>{let{args:t,force:r}=se(e);if(!r&&t.length===0)return;if(r){w.info(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){w.info(`${M} ${o}`);return}o!==void 0&&w.info(M,o)},warn:(...e)=>{w.warn(M,...e)},error:(...e)=>{w.error(M,...e)}}),ie=Object.freeze({dev:A,user:m});function Fe(e){return!e||typeof e!="object"?!1:Object.entries({dev:["log","info","debug","warn","error"],user:["log","info","warn","error"]}).every(([r,o])=>{let n=e[r];return!n||typeof n!="object"?!1:o.every(c=>typeof n[c]=="function")})}typeof window<"u"&&(Fe(window.bitvidLogger)||(window.bitvidLogger=ie));Object.freeze(ie);import{THEME_ACCENT_OVERRIDES as ce}from"../config/instance-config.js";var Z="bitvid:theme",O="dark",Pe=new Set(["light","dark"]),le={dark:"rgb(15, 23, 42)",light:"rgb(248, 250, 252)"},de=["[data-theme-toggle]",'[data-action="toggle-theme"]',".js-theme-toggle","#themeToggle"],pe=Object.freeze({accent:"--color-accent",accentStrong:"--color-accent-strong",accentPressed:"--color-accent-pressed"}),ue=new WeakSet,V=new Set,D=null,fe=!1,F=null,N=()=>typeof window<"u"&&typeof document<"u",W=e=>{if(typeof e!="string")return null;let t=e.trim().toLowerCase();return t==="default"||t===""?O:Pe.has(t)?t:null},Ve=()=>{if(!N())return null;try{let e=window.localStorage.getItem(Z);return W(e)}catch(e){return m.warn("Unable to read stored theme preference:",e),null}},We=e=>{if(N())try{window.localStorage.setItem(Z,e)}catch(t){m.warn("Unable to persist theme preference:",t)}},X=()=>{if(!N())return null;let{documentElement:e}=document;return e instanceof HTMLElement?e:null},qe=e=>typeof e=="string"?e.trim():"",je=e=>{if(!e||typeof e!="object")return null;let t=Object.entries(e).reduce((r,[o,n])=>{if(!n||typeof n!="object")return r;let c=Object.entries(n).reduce((a,[p,d])=>(Object.prototype.hasOwnProperty.call(pe,p)&&(a[p]=d),a),{});return Object.keys(c).length>0&&(r[o]=c),r},{});return Object.keys(t).length>0?t:null},me=e=>{F=je(e)};me(typeof ce<"u"?ce:null);var he=(e,t)=>{if(!e)return;let r=F&&typeof F=="object"&&F[t]&&typeof F[t]=="object"?F[t]:null,o=!1;Object.entries(pe).forEach(([n,c])=>{let a=r?r[n]:null,p=qe(a);p?(e.style.setProperty(c,p),o=!0):e.style.removeProperty(c)}),o?e.dataset.themeAccent="override":e.removeAttribute("data-theme-accent")},$e=e=>{if(!N())return;let t=document.querySelector('meta[name="theme-color"]');if(!t)return;let r=X();if(!r)return;let o="";try{typeof window.getComputedStyle=="function"&&(o=getComputedStyle(r).getPropertyValue("--color-page").trim())}catch(a){m.warn("Unable to compute theme color token:",a)}let n=le[e]||le[O],c=o||n;c&&t.setAttribute("content",c)},be=(e,t)=>{if(!(e instanceof HTMLElement))return;e.dataset.themeState=t,e.setAttribute("aria-pressed",t==="dark"?"true":"false");let r=t==="dark"?"Switch to light theme":"Switch to dark theme";e.setAttribute("aria-label",r),e.setAttribute("title",r);let o=e.querySelector("[data-theme-toggle-label]")||e.querySelector(".sr-only");o&&(o.textContent=r);let n=e.querySelector("[data-theme-toggle-icon]")||e.querySelector('[aria-hidden="true"]');if(n){let c=typeof SVGElement<"u"?SVGElement:null;if(c&&n instanceof c||n instanceof Element&&!!n.querySelector("svg"))n instanceof Element&&n.setAttribute("data-theme-icon-state",t);else{let p=e.dataset.iconLight||"\u2600\uFE0F",d=e.dataset.iconDark||"\u{1F319}";n.textContent=t==="dark"?p:d}}},Ye=()=>{if(!N()){V.clear();return}V.forEach(e=>{(!(e instanceof HTMLElement)||!document.contains(e))&&V.delete(e)})},Ge=e=>{Ye(),V.forEach(t=>{be(t,e)})},Ke=e=>{!(e instanceof HTMLElement)||ue.has(e)||(e.addEventListener("click",t=>{t.preventDefault(),Xe(D==="dark"?"light":"dark")}),e.dataset.themeToggleBound="true",ue.add(e))},ze=(e=document)=>{if(!N())return[];let t=new Set,r=e&&typeof e.querySelectorAll=="function"?e:document;return de.forEach(o=>{try{r.querySelectorAll(o).forEach(n=>{n instanceof HTMLElement&&t.add(n)})}catch(n){m.warn(`Invalid selector for theme toggle: ${o}`,n)}}),e instanceof HTMLElement&&de.forEach(o=>{e.matches(o)&&t.add(e)}),Array.from(t)},J=(e,{persist:t=!0}={})=>{let r=W(e)||O;D=r;let o=X();return o&&(o.dataset.theme=r,he(o,r)),t&&We(r),$e(r),Ge(r),r},Ee=e=>{me(e);let t=X();if(!t)return;he(t,D||O)},Xe=e=>N()?J(e,{persist:!0}):(D=W(e)||O,D),Ze=(e=document)=>{if(!N())return;ze(e).forEach(r=>{V.add(r),Ke(r),be(r,D||O)})},Je=()=>{if(!N())return O;let e=X();return(e?W(e.dataset.theme):null)||Ve()||O},Qe=e=>{if(!e||e.key!==Z)return;let t=W(e.newValue)||O;t!==D&&J(t,{persist:!1})},ye=()=>{if(!N()){D=O;return}fe||(window.addEventListener("storage",Qe),fe=!0);let e=Je();J(e,{persist:!0}),Ze(document)};var Te={info:"torrent-toast torrent-toast--info",success:"torrent-toast torrent-toast--success",error:"torrent-toast torrent-toast--error",warn:"torrent-toast torrent-toast--warn"},ge="transition duration-200 ease-out",et="beacon-toast-motion",Q="data-beacon-motion";function tt(e,t){let r=e?.defaultView;if(r&&typeof r.requestAnimationFrame=="function"){r.requestAnimationFrame(t);return}setTimeout(t,16)}function rt(e){let t=e||(typeof document<"u"?document:null);if(!t)return null;let r=t.getElementById("beacon-toast-container");if(!r){r=t.createElement("div"),r.id="beacon-toast-container",r.setAttribute("role","region"),r.setAttribute("aria-live","polite"),r.className=["pointer-events-none","fixed","inset-x-0","top-0","z-overlay-toast","flex","w-full","flex-col","items-center","gap-3","px-4","pt-6","sm:items-end","sm:px-6","sm:pt-8"].join(" ");let o=t.body||t.getElementsByTagName?.("body")?.[0];o&&o.appendChild(r)}return r}function nt(e,t,r){let o=e.createElement("div");o.className=[ge,r].join(" "),o.setAttribute("role","status"),o.setAttribute("tabindex","0"),o.textContent=t;let n=e.createElement("button");n.type="button",n.className=["notification-dismiss","focus-ring","absolute","right-3","top-3"].join(" "),n.setAttribute("aria-label","Dismiss notification"),n.innerHTML='<svg class="h-4 w-4" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.75 3.75l8.5 8.5"></path><path d="M12.25 3.75l-8.5 8.5"></path></svg>';let c=e.createElement("div");return c.className=["relative","w-full",ge,et].join(" "),c.setAttribute(Q,"exit"),c.appendChild(o),o.appendChild(n),{wrapper:c,toast:o,closeButton:n}}function ot(e,t=setTimeout,r=clearTimeout){return{show(o,n={}){if(!o)return;let c=n.type&&Te[n.type]?n.type:"info",a=rt(e);if(!a)return;let{wrapper:p,toast:d,closeButton:I}=nt(e,o,Te[c]);a.appendChild(p),tt(e,()=>{p.setAttribute(Q,"enter")});let L=typeof n.duration=="number"?n.duration:3500,E=()=>{p.setAttribute(Q,"exit");let T=t(()=>{p.parentNode&&p.parentNode.removeChild(p)},200);return I.removeEventListener("click",y),d.removeEventListener("keydown",h),I.removeEventListener("keydown",h),T},y=()=>E(),h=T=>{T.key==="Escape"&&(T.stopPropagation(),E())};I.addEventListener("click",y),d.addEventListener("keydown",h),I.addEventListener("keydown",h);let B=null;return n.sticky||(B=t(()=>{E()},L)),{dismiss(){B&&r(B),E()}}},set(o,n){return typeof n=="string"?this.show(o,{type:n}):this.show(o,n)},info(o,n={}){return this.show(o,{...n,type:"info"})},success(o,n={}){return this.show(o,{...n,type:"success"})},error(o,n={}){return this.show(o,{...n,type:"error"})},warn(o,n={}){return this.show(o,{...n,type:"warn"})}}}function _e(e,t=setTimeout,r=clearTimeout){return ot(e,t,r)}var ee=[{key:"name",label:"Name",headerClass:"w-full sm:w-1/3 text-left",cellClass:"font-medium text-text truncate"},{key:"progress",label:"Progress",headerClass:"w-24 text-right",cellClass:"tabular-nums text-right",formatter:"progress"},{key:"downloadSpeed",label:"\u2193 Speed",headerClass:"w-24 text-right",cellClass:"tabular-nums text-right",formatter:"bytesPerSecond"},{key:"uploadSpeed",label:"\u2191 Speed",headerClass:"w-24 text-right",cellClass:"tabular-nums text-right",formatter:"bytesPerSecond"},{key:"numPeers",label:"Peers",headerClass:"w-20 text-right",cellClass:"tabular-nums text-right",formatter:"integer"},{key:"ratio",label:"Ratio",headerClass:"w-20 text-right",cellClass:"tabular-nums text-right",formatter:"ratio"}];function at(e){return e?e.infoHash||e.magnetURI||e.name||String(Math.random()):""}function st(e,t){return!e||!t?!1:e===t||e.magnetURI&&e.magnetURI===t.magnetURI||e.infoHash&&e.infoHash===t.infoHash}function it(e){return typeof e!="number"||Number.isNaN(e)?"":`${(e*100).toFixed(1)}%`}function ct(e){return typeof e!="number"||!Number.isFinite(e)?"0.00":e.toFixed(2)}function lt(e){return typeof e!="number"||!Number.isFinite(e)?"0":Math.max(0,Math.floor(e)).toString()}function dt(e,t){if(typeof t=="function")return t(e,!0)||"";if(typeof e!="number"||Number.isNaN(e)||e<1)return"";let r=["B","kB","MB","GB","TB"],o=Math.min(Math.floor(Math.log(e)/Math.log(1e3)),r.length-1);return`${(e/Math.pow(1e3,o)).toFixed(1)} ${r[o]}/s`}function ut(e,t){let r=e.formatter;if(typeof r=="function")return r;let o=t?.[r];if(typeof o=="function")return o;switch(r){case"progress":return it;case"ratio":return ct;case"integer":return lt;case"bytesPerSecond":return n=>dt(n,t?.bytes);default:return n=>n}}function Se({documentRef:e=typeof document<"u"?document:null,root:t,onSelect:r,formatters:o={}}={}){if(!e)throw new Error("createTorrentTable requires a document reference");if(!(t instanceof e.defaultView.HTMLElement))throw new Error("createTorrentTable requires a valid root element");let n=e;t.innerHTML="";let c=n.createElement("div");c.className="overflow-hidden rounded-xl border border-border-translucent bg-surface shadow-sm";let a=n.createElement("div");a.className="overflow-x-auto",c.appendChild(a);let p=n.createElement("table");p.className="min-w-full divide-y divide-border-translucent text-sm",a.appendChild(p);let d=n.createElement("thead");d.className="bg-surface-alt text-muted-strong";let I=n.createElement("tr");ee.forEach(C=>{let v=n.createElement("th");v.setAttribute("scope","col"),v.className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide",C.headerClass&&(v.className+=` ${C.headerClass}`),v.textContent=C.label,I.appendChild(v)}),d.appendChild(I),p.appendChild(d);let L=n.createElement("tbody");L.className="divide-y divide-border-translucent",p.appendChild(L);let E=n.createElement("tbody"),y=n.createElement("tr"),h=n.createElement("td");h.className="px-4 py-8 text-center text-sm text-muted",h.colSpan=ee.length,h.textContent="No active torrents yet. Add a magnet link to get started.",y.appendChild(h),E.appendChild(y),p.appendChild(E),t.appendChild(c);function B(C=[],v=null){let $=Array.isArray(C)?C:[];if(L.innerHTML="",!$.length){L.hidden=!0,E.hidden=!1;return}L.hidden=!1,E.hidden=!0;for(let g of $){let _=n.createElement("tr");_.className=["group","align-middle","cursor-pointer","transition","hover:bg-panel-hover","focus-visible:outline","focus-visible:outline-2","focus-visible:outline-offset-2","focus-visible:outline-info"].join(" "),_.setAttribute("role","button"),_.setAttribute("tabindex","0"),_.dataset.torrentId=at(g),st(v,g)?(_.classList.add("bg-info/10","text-text"),_.setAttribute("aria-pressed","true")):_.setAttribute("aria-pressed","false"),_.addEventListener("click",()=>{typeof r=="function"&&r(g)}),_.addEventListener("keydown",x=>{(x.key==="Enter"||x.key===" ")&&(x.preventDefault(),typeof r=="function"&&r(g))}),ee.forEach(x=>{let H=n.createElement("td");H.className="px-4 py-3",x.cellClass&&(H.className+=` ${x.cellClass}`);let Y=g?g[x.key]:void 0,G=ut(x,o)(Y,g,x),K=n.createElement("span");K.textContent=G==null?"":String(G),H.appendChild(K),_.appendChild(H)}),L.appendChild(_)}}function T(){t.innerHTML=""}return{render:B,destroy:T}}var ft="2.0",Ce=["wss://tracker.btorrent.xyz","wss://tracker.openwebtorrent.com"],we={announce:Ce},pt={announce:Ce},mt=3e4;function q(e,t=!1){if(typeof e!="number"||Number.isNaN(e))return"";if(e<1)return t?"":"0 B";let r=["B","kB","MB","GB","TB"],o=Math.min(Math.floor(Math.log(e)/Math.log(1e3)),r.length-1);return`${(e/Math.pow(1e3,o)).toFixed(1)} ${r[o]}${t?"/s":""}`}function ht(e){return typeof e!="number"||Number.isNaN(e)?"":`${(e*100).toFixed(1)}%`}function Ae(e){return typeof e!="number"||!Number.isFinite(e)?"0.00":e.toFixed(2)}function bt(e,t){return!e||!t?!1:!!(e===t||e.infoHash&&t.infoHash&&e.infoHash===t.infoHash||e.magnetURI&&t.magnetURI&&e.magnetURI===t.magnetURI)}function Et(e){if(!(!e||!Array.isArray(e.files))&&(e.files.forEach(t=>{typeof t.priority!="string"&&(t.priority="0"),typeof t.getBlobURL=="function"&&t.getBlobURL((r,o)=>{r||!o||(t.url=o)})}),e.torrentFile&&typeof Blob=="function"&&typeof URL<"u"))try{let t=new Blob([e.torrentFile],{type:"application/x-bittorrent"});if(e.torrentFileBlobURL=URL.createObjectURL(t),!e.fileName){let r=e.name||"download";e.fileName=`${r}.torrent`}}catch(t){A.warn("[beacon] Failed to create torrent blob URL",t)}}function yt(e,t){if(!e)return;let r=typeof t=="string"?t:String(t??"0");if(e.priority=r,r==="-1"){typeof e.deselect=="function"&&e.deselect();return}let o=Number.parseInt(r,10);Number.isFinite(o)&&typeof e.select=="function"&&e.select(o)}function Le(e,t){if(!Array.isArray(t)||t.length===0)return null;if(!e)return t[0];let r=t.find(o=>bt(o,e));return r||t[0]}function xe({documentRef:e=typeof document<"u"?document:null,WebTorrentCtor:t}={}){if(!e)throw new Error("createBeaconApp requires a document reference");let r=e.defaultView||globalThis,o=typeof t=="function"?t:typeof r?.WebTorrent=="function"?r.WebTorrent:null;if(!o)throw new Error("createBeaconApp requires a WebTorrent constructor");let n=_e(e),c=new o({tracker:pt}),a={form:e.querySelector('[data-beacon="magnet-form"]'),magnetInput:e.querySelector('[data-beacon="magnet-input"]'),seedButton:e.querySelector('[data-beacon="seed-button"]'),seedInput:e.querySelector('[data-beacon="seed-input"]'),tableRoot:e.querySelector('[data-beacon="torrent-table"]'),selectedSection:e.querySelector('[data-beacon="selected"]'),selectedName:e.querySelector('[data-beacon="selected-name"]'),pauseButton:e.querySelector('[data-beacon-action="pause"]'),resumeButton:e.querySelector('[data-beacon-action="resume"]'),downloadAllButton:e.querySelector('[data-beacon-action="download-all"]'),removeButton:e.querySelector('[data-beacon-action="remove"]'),copyMagnetButton:e.querySelector('[data-beacon-action="copy-magnet"]'),infoHashValue:e.querySelector('[data-beacon="info-hash"]'),fileList:e.querySelector('[data-beacon="file-list"]'),stats:e.querySelector('[data-beacon="client-stats"]'),overlay:e.querySelector('[data-beacon="processing-overlay"]')},p=a.tableRoot?Se({documentRef:e,root:a.tableRoot,formatters:{bytes:(s,i=!1)=>q(s,i),progress:s=>ht(s),ratio:s=>Ae(s),integer:s=>typeof s=="number"&&Number.isFinite(s)?Math.floor(s).toString():"0",bytesPerSecond:s=>q(s,!0)},onSelect(s){d.selectedTorrent=s,g()}}):null,d={selectedTorrent:null,processing:!1},I=[],L=[],E=null;function y(s){I.push(s)}function h(){E!==null&&(r.clearTimeout(E),E=null)}function B(){T(!0),h(),E=r.setTimeout(()=>{E=null,d.processing&&(T(!1),n?.warn("Still searching for peers \u2014 this is taking a while. It'll keep trying in the background."))},mt)}function T(s){if(d.processing=!!s,!a.overlay)return;let i=d.processing;a.overlay.hidden=!i,a.overlay.setAttribute("aria-hidden",i?"false":"true"),i?a.overlay.style.removeProperty("display"):a.overlay.style.display="none"}function C(){return Array.isArray(c.torrents)?[...c.torrents]:[]}function v(){if(!a.stats)return;let s=q(c.downloadSpeed,!0)||"0 B/s",i=q(c.uploadSpeed,!0)||"0 B/s",u=Ae(c.ratio);a.stats.textContent=`Client stats: \u2193 ${s} \xB7 \u2191 ${i} \xB7 Ratio: ${u}`}function $(s){a.fileList&&(a.fileList.innerHTML="",!(!s||!Array.isArray(s.files)||!s.files.length)&&s.files.forEach((i,u)=>{let f=e.createElement("tr");f.className="align-top";let l=e.createElement("td");if(l.className="px-3 py-2",i.done&&i.url){let S=e.createElement("a");S.className="text-info hover:text-info-strong",S.href=i.url,S.download=i.name||`file-${u+1}`,S.textContent=i.name||`File ${u+1}`,l.appendChild(S)}else{let S=e.createElement("span");S.textContent=i.name||`File ${u+1}`,l.appendChild(S)}f.appendChild(l);let b=e.createElement("td");b.className="px-3 py-2 text-muted-strong",b.textContent=q(i.length)||"0 B",f.appendChild(b);let P=e.createElement("td");P.className="px-3 py-2";let R=e.createElement("select");R.className="select",R.name=`${i.name||`file-${u+1}`}-priority`,R.setAttribute("aria-label",`Set priority for ${i.name||`file ${u+1}`}`),[{value:"1",label:"High priority"},{value:"0",label:"Low priority"},{value:"-1",label:"Don't download"}].forEach(S=>{let z=e.createElement("option");z.value=S.value,z.textContent=S.label,S.value===i.priority&&(z.selected=!0),R.appendChild(z)}),R.value=i.priority??"0",R.addEventListener("change",S=>{yt(i,S.target.value)}),P.appendChild(R),f.appendChild(P),a.fileList.appendChild(f)}))}function g(){if(!a.selectedSection)return;let s=C();d.selectedTorrent=Le(d.selectedTorrent,s);let i=d.selectedTorrent;a.selectedSection.hidden=!i,i&&(a.selectedName&&(a.selectedName.textContent=i.name||"Untitled torrent"),a.pauseButton&&(a.pauseButton.hidden=i.paused===!0,a.pauseButton.disabled=i.paused===!0),a.resumeButton&&(a.resumeButton.hidden=i.paused!==!0,a.resumeButton.disabled=i.paused!==!0),a.downloadAllButton&&(a.downloadAllButton.disabled=!(i.progress>=1)),a.infoHashValue&&(a.infoHashValue.textContent=i.infoHash||""),$(i))}function _(s,{isSeed:i=!1}={}){h(),T(!1),Et(s),i||n.info(`Received ${s.name||"torrent"} metadata`),d.selectedTorrent||(d.selectedTorrent=s),g(),p?.render(C(),d.selectedTorrent)}function x(s){let i=typeof s=="string"?s.trim():"";if(!i)return;B();let u;try{u=c.add(i,we,f=>{_(f,{isSeed:!1})})}catch(f){h(),T(!1),m.error("[beacon] Failed to add magnet",f),n?.error(f?.message||"Couldn't add that magnet.");return}Y(u)}function H(s){if(!Array.isArray(s)||s.length===0)return;B();let i;try{i=c.seed(s,we,u=>{_(u,{isSeed:!0}),n.success(`Seeding ${u.files.length} file(s)`)})}catch(u){h(),T(!1),m.error("[beacon] Failed to seed files",u),n?.error(u?.message||"Couldn't seed those files.");return}Y(i)}function Y(s){!s||typeof s.once!="function"||s.once("error",i=>{h(),T(!1),m.error("[beacon] Torrent error",i),n?.error(i?.message||"That torrent could not be loaded.")})}function te(s){if(s){if(s.progress<1){n.warn("Torrent is not finished downloading yet.");return}s.files.forEach(i=>{typeof i.getBlob=="function"&&i.getBlob((u,f)=>{if(u||!f){m.error("[beacon] Failed to get blob for file",i?.name,u),n.error("Failed to prepare file for download.");return}let l=URL.createObjectURL(f),b=e.createElement("a");b.className="beacon-hidden-download",b.setAttribute("data-beacon-hidden-download","true"),b.href=l,b.download=i.name||"download",e.body.appendChild(b),b.click(),e.body.removeChild(b),URL.revokeObjectURL(l)})})}}function G(s){if(!s?.magnetURI)return;let i=s.magnetURI,u=r?.navigator?.clipboard;if(u&&typeof u.writeText=="function"){u.writeText(i).then(()=>{n.success("Magnet URI copied to clipboard!")}).catch(f=>{m.error("[beacon] Clipboard error",f),n.error("Failed to copy magnet URI")});return}try{let f=e.createElement("textarea");f.value=i,f.className="beacon-clipboard-offscreen",f.setAttribute("data-beacon-clipboard-state","offscreen"),e.body.appendChild(f),f.select(),e.execCommand?.("copy"),e.body.removeChild(f),n.success("Magnet URI copied (fallback)!")}catch(f){m.error("[beacon] Clipboard fallback error",f),n.error("Failed to copy magnet URI")}}function K(s){s&&s.destroy(i=>{i&&(m.error("[beacon] Failed to destroy torrent",i),n.error("Failed to remove torrent"));let u=C();d.selectedTorrent=Le(null,u),p?.render(u,d.selectedTorrent),g()})}function Oe(){if(A.info(`[beacon] Starting beacon runtime v${ft}`),n||A.warn("[beacon] Toast service unavailable"),o.WEBRTC_SUPPORT||n?.error("Please use a browser with WebRTC support.",{sticky:!0}),a.form){let l=b=>{b.preventDefault(),x(a.magnetInput?.value||""),a.magnetInput&&(a.magnetInput.value="")};a.form.addEventListener("submit",l),y(()=>a.form.removeEventListener("submit",l))}if(a.seedButton&&a.seedInput){let l=()=>{a.seedInput.click()},b=()=>{let P=Array.from(a.seedInput.files||[]);a.seedInput.value="",H(P)};a.seedButton.addEventListener("click",l),a.seedInput.addEventListener("change",b),y(()=>{a.seedButton.removeEventListener("click",l),a.seedInput.removeEventListener("change",b)})}if(a.pauseButton){let l=()=>{d.selectedTorrent&&typeof d.selectedTorrent.pause=="function"&&(d.selectedTorrent.pause(),g())};a.pauseButton.addEventListener("click",l),y(()=>a.pauseButton.removeEventListener("click",l))}if(a.resumeButton){let l=()=>{d.selectedTorrent&&typeof d.selectedTorrent.resume=="function"&&(d.selectedTorrent.resume(),g())};a.resumeButton.addEventListener("click",l),y(()=>a.resumeButton.removeEventListener("click",l))}if(a.downloadAllButton){let l=()=>te(d.selectedTorrent);a.downloadAllButton.addEventListener("click",l),y(()=>a.downloadAllButton.removeEventListener("click",l))}if(a.removeButton){let l=()=>K(d.selectedTorrent);a.removeButton.addEventListener("click",l),y(()=>a.removeButton.removeEventListener("click",l))}if(a.copyMagnetButton){let l=()=>G(d.selectedTorrent);a.copyMagnetButton.addEventListener("click",l),y(()=>a.copyMagnetButton.removeEventListener("click",l))}let s=l=>{m.error("[beacon] Torrent client error",l),n?.error(l?.message||String(l)),h(),T(!1)};c.on("error",s),y(()=>c.removeListener("error",s));let i=l=>{if(c.torrents&&c.torrents.length>0)return l.preventDefault(),l.returnValue="Transfers are in progress. Are you sure you want to leave or refresh?",l.returnValue};r&&typeof r.addEventListener=="function"&&(r.addEventListener("beforeunload",i),y(()=>r.removeEventListener("beforeunload",i))),p?.render(C(),d.selectedTorrent),g(),v(),T(!1);let u=r.setInterval(()=>{p?.render(C(),d.selectedTorrent),g(),v()},1e3);L.push(u);let f=r?.location?.hash;typeof f=="string"&&f.length>1&&x(f.slice(1))}function Ne(){h(),L.forEach(s=>r.clearInterval(s)),L.length=0,I.forEach(s=>{try{s()}catch(i){A.warn("[beacon] Cleanup task failed",i)}}),I.length=0,p?.destroy();try{c.destroy()}catch(s){A.warn("[beacon] Failed to destroy client",s)}}return{mount:Oe,destroy:Ne}}var Ie="../../config/instance-config.js";function Tt(){return typeof window<"u"&&window||typeof globalThis<"u"&&globalThis||null}function gt(){let e=Tt();if(e&&typeof e.WebTorrent=="function")return e.WebTorrent;throw new Error("WebTorrent runtime is not available on the global scope")}var j=null,_t=async()=>{try{let e=await import(Ie);if(e&&typeof e.THEME_ACCENT_OVERRIDES<"u")return e.THEME_ACCENT_OVERRIDES}catch(e){A.warn(`[beacon] Failed to load theme accent overrides from ${Ie}`,e)}return null},St=async()=>{try{let e=await _t();e&&Ee(e)}catch(e){A.warn("[beacon] Unable to hydrate theme accent overrides",e)}try{ye()}catch(e){A.warn("[beacon] Failed to initialize theme controller",e)}};St();function ve(){if(j)return A.info("[beacon] Reusing existing beacon app instance"),j;if(typeof document>"u")throw new Error("Beacon runtime requires a document environment");let e=gt();return A.info("[beacon] Creating beacon app instance"),j=xe({documentRef:document,WebTorrentCtor:e}),j.mount(),j}if(typeof document<"u")if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",()=>{try{ve()}catch(e){m.error("[beacon] Failed to mount app",e)}});else try{ve()}catch(e){m.error("[beacon] Failed to mount app",e)}export{xe as createBeaconApp,ve as mountBeaconApp};