smart queue fix
This commit is contained in:
@@ -181,34 +181,63 @@ def _restore_auto_label(client: Any, profile_id: int, torrent_hash: str, current
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_rtorrent_download_cap(client: Any, max_active: int) -> dict[str, Any]:
|
def _call_rtorrent_setter(client: Any, method: str, value: int) -> bool:
|
||||||
"""Raise rTorrent's own download cap when it is lower than Smart Queue's target."""
|
"""Set a scalar rTorrent setting while tolerating XMLRPC signature differences."""
|
||||||
result: dict[str, Any] = {'checked': False, 'updated': False}
|
for args in ((int(value),), ('', int(value))):
|
||||||
try:
|
try:
|
||||||
current = int(client.call('throttle.max_downloads.global') or 0)
|
client.call(method, *args)
|
||||||
result.update({'checked': True, 'current': current, 'target': max_active})
|
return True
|
||||||
# Note: 0 means unlimited in rTorrent, so only smaller positive caps are raised.
|
|
||||||
if 0 < current < max_active:
|
|
||||||
try:
|
|
||||||
client.call('throttle.max_downloads.global.set', '', int(max_active))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
client.call('throttle.max_downloads.global.set', int(max_active))
|
continue
|
||||||
result.update({'updated': True, 'new': int(max_active)})
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_rtorrent_download_cap(client: Any, max_active: int) -> dict[str, Any]:
|
||||||
|
"""Raise rTorrent download caps that can silently limit Smart Queue to one item."""
|
||||||
|
result: dict[str, Any] = {'checked': False, 'updated': False, 'items': []}
|
||||||
|
# Note: rTorrent może mieć osobny limit globalny i per-throttle. Gdy div=1,
|
||||||
|
# startowanie kończy się praktycznie jednym aktywnym torrentem mimo targetu 100.
|
||||||
|
for key in ('throttle.max_downloads.global', 'throttle.max_downloads.div'):
|
||||||
|
item: dict[str, Any] = {'key': key, 'checked': False, 'updated': False}
|
||||||
|
try:
|
||||||
|
current = int(client.call(key) or 0)
|
||||||
|
item.update({'checked': True, 'current': current, 'target': int(max_active)})
|
||||||
|
result['checked'] = True
|
||||||
|
# Note: 0 oznacza unlimited; podnosimy tylko dodatnie limity niższe od targetu.
|
||||||
|
if 0 < current < max_active:
|
||||||
|
ok = _call_rtorrent_setter(client, f'{key}.set', int(max_active))
|
||||||
|
item['updated'] = ok
|
||||||
|
if ok:
|
||||||
|
result['updated'] = True
|
||||||
|
item['new'] = int(max_active)
|
||||||
|
result.setdefault('current', current)
|
||||||
|
result['new'] = int(max_active)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# Note: Missing/older rTorrent throttle RPC should not block queue processing.
|
item.update({'error': str(exc)})
|
||||||
result.update({'error': str(exc)})
|
result['items'].append(item)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _start_download(client: Any, torrent: dict[str, Any]) -> None:
|
def _start_download(client: Any, torrent: dict[str, Any]) -> None:
|
||||||
"""Resume paused torrents and start stopped torrents using the smallest safe RPC sequence."""
|
"""Resume paused torrents and open/start stopped torrents with a tolerant RPC sequence."""
|
||||||
h = str(torrent.get('hash') or '')
|
h = str(torrent.get('hash') or '')
|
||||||
if not h:
|
if not h:
|
||||||
return
|
return
|
||||||
# Note: Paused wymaga resume+start; stopped startujemy bez resume, gdy ustawienie na to pozwala.
|
# Note: d.pause zostawia torrent w state=1, ale active=0; samo d.start często nic nie zmienia.
|
||||||
if bool(torrent.get('paused')):
|
# Dlatego dla pozycji paused zawsze wysyłamy d.resume, a dla stopped próbujemy d.open przed d.start.
|
||||||
|
if bool(torrent.get('paused')) or int(torrent.get('state') or 0):
|
||||||
client.call('d.resume', h)
|
client.call('d.resume', h)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
client.call('d.open', h)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
client.call('d.start', h)
|
client.call('d.start', h)
|
||||||
|
if bool(torrent.get('paused')):
|
||||||
|
try:
|
||||||
|
client.call('d.resume', h)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _verify_started_downloads(client: Any, hashes: list[str], attempts: int = 3, delay: float = 0.25) -> tuple[list[str], list[dict[str, Any]]]:
|
def _verify_started_downloads(client: Any, hashes: list[str], attempts: int = 3, delay: float = 0.25) -> tuple[list[str], list[dict[str, Any]]]:
|
||||||
@@ -243,9 +272,9 @@ def _read_live_start_state(client: Any, torrent_hash: str) -> dict[str, Any]:
|
|||||||
result[key] = int(value or 0) if key in {'state', 'active'} else str(value or '')
|
result[key] = int(value or 0) if key in {'state', 'active'} else str(value or '')
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
result[f'{key}_error'] = str(exc)
|
result[f'{key}_error'] = str(exc)
|
||||||
# Note: rTorrent d.is_active oznacza realny transfer/aktywne peery, a nie slot kolejki.
|
# Note: Dla Smart Queue slot aktywny musi zniknąć z UI jako Paused, więc wymagamy active=1.
|
||||||
# Torrenty uruchomione, ale czekajace na peery/throttle, maja state=1 i active=0.
|
# state=1 alone może oznaczać nadal zapauzowany torrent po d.pause.
|
||||||
result['started'] = bool(int(result.get('state') or 0))
|
result['started'] = bool(int(result.get('state') or 0)) and bool(int(result.get('active') or 0))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _set_smart_queue_label(client: Any, torrent_hash: str, attempts: int = 3) -> bool:
|
def _set_smart_queue_label(client: Any, torrent_hash: str, attempts: int = 3) -> bool:
|
||||||
@@ -321,6 +350,27 @@ def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str,
|
|||||||
return restored
|
return restored
|
||||||
|
|
||||||
|
|
||||||
|
def _is_running_download_slot(t: dict[str, Any]) -> bool:
|
||||||
|
"""Return True only for torrents that occupy a visible active download slot."""
|
||||||
|
# Note: normalize_row oznacza state=1/active=0 jako Paused; takich nie liczymy jako aktywne sloty.
|
||||||
|
return (
|
||||||
|
not int(t.get('complete') or 0)
|
||||||
|
and int(t.get('state') or 0)
|
||||||
|
and not bool(t.get('paused'))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_waiting_download_candidate(t: dict[str, Any], manage_stopped: bool) -> bool:
|
||||||
|
"""Return True for paused/held torrents Smart Queue may resume later."""
|
||||||
|
if int(t.get('complete') or 0):
|
||||||
|
return False
|
||||||
|
if str(t.get('label') or '') == SMART_QUEUE_LABEL:
|
||||||
|
return True
|
||||||
|
if bool(t.get('paused')):
|
||||||
|
return True
|
||||||
|
return bool(manage_stopped) and not int(t.get('state') or 0)
|
||||||
|
|
||||||
|
|
||||||
def check(profile: dict | None = None, user_id: int | None = None, force: bool = False) -> dict[str, Any]:
|
def check(profile: dict | None = None, user_id: int | None = None, force: bool = False) -> dict[str, Any]:
|
||||||
profile = profile or active_profile()
|
profile = profile or active_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
@@ -349,18 +399,17 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
# dla torrentu juz wystartowanego, ale chwilowo bez transferu, wiec powodowal startowanie po jednej sztuce.
|
# dla torrentu juz wystartowanego, ale chwilowo bez transferu, wiec powodowal startowanie po jednej sztuce.
|
||||||
downloading = [
|
downloading = [
|
||||||
t for t in torrents
|
t for t in torrents
|
||||||
if not int(t.get('complete') or 0)
|
if _is_running_download_slot(t)
|
||||||
and int(t.get('state') or 0)
|
|
||||||
and not is_managed_hold(t)
|
and not is_managed_hold(t)
|
||||||
and t.get('hash') not in excluded
|
and t.get('hash') not in excluded
|
||||||
]
|
]
|
||||||
# Note: Kandydaci do uruchomienia to przede wszystkim torrenty odlozone przez Smart Queue.
|
# Note: Kandydaci obejmują także zwykłe Paused bez labela. Inaczej kolejka widzi tylko 1-2 sztuki
|
||||||
# Nie traktujemy kazdego state=1/active=0 jako pauzy, bo rTorrent tak pokazuje tez oczekiwanie na peery/throttle.
|
# i nie potrafi dobić do zadanego targetu 100.
|
||||||
stopped = [
|
stopped = [
|
||||||
t for t in torrents
|
t for t in torrents
|
||||||
if not int(t.get('complete') or 0)
|
if t.get('hash') not in excluded
|
||||||
and t.get('hash') not in excluded
|
and _is_waiting_download_candidate(t, manage_stopped)
|
||||||
and (is_managed_hold(t) or (manage_stopped and not int(t.get('state') or 0)))
|
and not _is_running_download_slot(t)
|
||||||
]
|
]
|
||||||
min_speed = int(settings.get('min_speed_bytes') or 0)
|
min_speed = int(settings.get('min_speed_bytes') or 0)
|
||||||
min_seeds = int(settings.get('min_seeds') or 0)
|
min_seeds = int(settings.get('min_seeds') or 0)
|
||||||
@@ -422,6 +471,8 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
active_after_pause = max(0, len(downloading) - len(to_pause))
|
active_after_pause = max(0, len(downloading) - len(to_pause))
|
||||||
available_slots = max(0, max_active - active_after_pause)
|
available_slots = max(0, max_active - active_after_pause)
|
||||||
to_resume = candidates[:available_slots]
|
to_resume = candidates[:available_slots]
|
||||||
|
# Note: Pozycje poza bieżącą pulą startu zostają jawnie oznaczone jako oczekujące Smart Queue.
|
||||||
|
to_label_waiting = candidates[available_slots:]
|
||||||
|
|
||||||
c = rtorrent.client_for(profile)
|
c = rtorrent.client_for(profile)
|
||||||
rtorrent_cap = _ensure_rtorrent_download_cap(c, max_active)
|
rtorrent_cap = _ensure_rtorrent_download_cap(c, max_active)
|
||||||
@@ -441,13 +492,22 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
for t in to_label_waiting:
|
||||||
|
h = str(t.get('hash') or '')
|
||||||
|
if not h or h in pause_hashes:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if not _mark_auto_paused(c, profile_id, t):
|
||||||
|
label_failed.append(h)
|
||||||
|
except Exception:
|
||||||
|
label_failed.append(h)
|
||||||
|
|
||||||
# Note: Startujemy całą pulę kandydatów w jednej rundzie, a dopiero potem weryfikujemy efekt.
|
# Note: Startujemy całą pulę kandydatów w jednej rundzie, a dopiero potem weryfikujemy efekt.
|
||||||
for t in to_resume:
|
for t in to_resume:
|
||||||
h = str(t.get('hash') or '')
|
h = str(t.get('hash') or '')
|
||||||
if not h:
|
if not h:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
_restore_auto_label(c, profile_id, h, str(t.get('label') or ''))
|
|
||||||
_start_download(c, t)
|
_start_download(c, t)
|
||||||
resume_requested.append(h)
|
resume_requested.append(h)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -457,8 +517,12 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
for h in verified:
|
for h in verified:
|
||||||
_restore_auto_label(c, profile_id, h, None)
|
_restore_auto_label(c, profile_id, h, None)
|
||||||
resumed = verified
|
resumed = verified
|
||||||
keep_labels = set(paused) | {str(t.get('hash') or '') for t in stopped if str(t.get('label') or '') == SMART_QUEUE_LABEL and str(t.get('hash') or '') not in set(resumed)}
|
keep_labels = (
|
||||||
|
set(paused)
|
||||||
|
| {str(t.get('hash') or '') for t in to_label_waiting}
|
||||||
|
| {str(t.get('hash') or '') for t in stopped if str(t.get('label') or '') == SMART_QUEUE_LABEL and str(t.get('hash') or '') not in set(resumed)}
|
||||||
|
)
|
||||||
restored = _cleanup_auto_labels(c, profile_id, torrents, keep_labels, manage_stopped)
|
restored = _cleanup_auto_labels(c, profile_id, torrents, keep_labels, manage_stopped)
|
||||||
details = {'excluded': len(excluded), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'resume_requested': resume_requested, 'manage_stopped': manage_stopped, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after': active_after_pause + len(resumed), 'rtorrent_cap': rtorrent_cap}
|
details = {'excluded': len(excluded), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'resume_requested': resume_requested, 'waiting_labeled': len(to_label_waiting), 'manage_stopped': manage_stopped, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after': active_after_pause + len(resumed), 'rtorrent_cap': rtorrent_cap}
|
||||||
add_history(profile_id, 'force_check' if force else 'auto_check', paused, resumed, len(torrents), details, user_id)
|
add_history(profile_id, 'force_check' if force else 'auto_check', paused, resumed, len(torrents), details, user_id)
|
||||||
return {'ok': True, 'enabled': bool(settings.get('enabled')), 'paused': paused, 'resumed': resumed, 'resume_requested': resume_requested, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings}
|
return {'ok': True, 'enabled': bool(settings.get('enabled')), 'paused': paused, 'resumed': resumed, 'resume_requested': resume_requested, 'waiting_labeled': len(to_label_waiting), 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings}
|
||||||
|
|||||||
@@ -669,7 +669,7 @@
|
|||||||
}catch(e){ box.innerHTML=`<div class="text-danger">${esc(e.message)}</div>`; }
|
}catch(e){ box.innerHTML=`<div class="text-danger">${esc(e.message)}</div>`; }
|
||||||
}
|
}
|
||||||
|
|
||||||
$('toolsModal')?.addEventListener('show.bs.modal',()=>{refreshProfiles();loadLabels();loadRatios();loadRss();loadSmartQueue();loadRtConfig();loadAutomations();loadCleanup();loadAppStatus();loadPreferences();renderColumnManager();applyColumnVisibility();updateAutomationForm();}); const toolPanelIds={rtorrents:'toolRtorrents',settings:'toolRtorrents',torrentstats:'toolTorrentStats',preferences:'toolPreferences',labels:'toolLabels',ratio:'toolRatio',rss:'toolRss',columns:'toolColumns',smart:'toolSmart',automations:'toolAutomations',rtconfig:'toolRtconfig',cleanup:'toolCleanup',appstatus:'toolAppstatus'}; const hideToolPanels=()=>Object.values(toolPanelIds).filter((v,i,a)=>a.indexOf(v)===i).forEach(id=>$(id)?.classList.add('d-none')); const showToolPanel=tool=>{hideToolPanels(); $(toolPanelIds[tool]||'toolRtorrents')?.classList.remove('d-none');}; const activateToolTab=tool=>{document.querySelectorAll('.tool-tab').forEach(x=>x.classList.toggle('active',(x.dataset.tool||'rtorrents')===tool)); showToolPanel(tool); if(tool==='torrentstats') loadTorrentStats(false); if(tool==='appstatus') loadAppStatus(); if(tool==='cleanup') loadCleanup(); if(tool==='preferences') loadPreferences();}; document.querySelectorAll('.tool-tab').forEach(b=>b.addEventListener('click',()=>activateToolTab(b.dataset.tool||'rtorrents'))); $('torrentStatsRefreshBtn')?.addEventListener('click',()=>loadTorrentStats(true)); $('rssFeedBtn')?.addEventListener('click',async()=>{await post('/api/rss/feeds',{name:$('rssName').value,url:$('rssUrl').value}); loadRss();}); $('rssRuleBtn')?.addEventListener('click',async()=>{await post('/api/rss/rules',{name:$('rssRuleName').value,pattern:$('rssPattern').value,save_path:$('rssPath').value,label:$('rssLabel').value}); loadRss();}); $('rssCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/rss/check',{}); toast(`RSS queued ${j.queued} item(s)`,'success');}catch(e){toast(e.message,'danger');} finally{setBusy(false);}}); $('smartSaveBtn')?.addEventListener('click',saveSmartQueue); $('smartCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/smart-queue/check',{}); const r=j.result||{}; if(j.torrent_patch) patchRows(j.torrent_patch); const noEffect=r.start_no_effect?.length||0; const requested=r.resume_requested?.length||0; const cap=r.rtorrent_cap?.updated?`, cap ${r.rtorrent_cap.current}->${r.rtorrent_cap.new}`:''; const tail=noEffect?`, no effect ${noEffect}`:requested?`, requested ${requested}`:''; toast(`Smart Queue: paused ${r.paused?.length||0}, resumed ${r.resumed?.length||0}${tail}${cap}`,'success'); await loadSmartQueue();}catch(e){toast(e.message,'danger');}finally{setBusy(false);}}); $('smartManager')?.addEventListener('click',async e=>{const h=e.target.closest('.smart-unexclude')?.dataset.hash; if(!h)return; await post('/api/smart-queue/exclusion',{hash:h,excluded:false}); await loadSmartQueue();}); $('cleanupManager')?.addEventListener('click',async e=>{ if(e.target.closest('#cleanupRefreshBtn')) return loadCleanup(); if(e.target.closest('#cleanupJobsBtn')) return runCleanupAction('/api/cleanup/jobs','Clear finished job logs'); if(e.target.closest('#cleanupSmartQueueBtn')) return runCleanupAction('/api/cleanup/smart-queue','Clear Smart Queue logs'); if(e.target.closest('#cleanupAllBtn')) return runCleanupAction('/api/cleanup/all','Clear job and Smart Queue logs'); }); $('rtConfigReloadBtn')?.addEventListener('click',loadRtConfig); $('rtConfigSaveBtn')?.addEventListener('click',saveRtConfig); $('rtConfigGenerateBtn')?.addEventListener('click',generateRtConfig); $('rtConfigManager')?.addEventListener('input',e=>{ if(e.target.classList.contains('rt-config-input')) updateRtConfigDirty(); }); $('rtConfigManager')?.addEventListener('change',e=>{ if(e.target.classList.contains('rt-config-input')){ const label=e.target.closest('.rt-config-switch')?.querySelector('.form-check-label'); if(label) label.textContent=e.target.checked?'On':'Off'; updateRtConfigDirty(); } }); $('rtConfigApplyOnStart')?.addEventListener('change',updateRtConfigDirty); $('peersRefreshSelect')?.addEventListener('change',async e=>{peersRefreshSeconds=Number(e.target.value||0); await post('/api/preferences',{peers_refresh_seconds:peersRefreshSeconds}).catch(()=>{}); setupPeersRefresh(activeTab()); toast('Peers refresh preference saved','success');});
|
$('toolsModal')?.addEventListener('show.bs.modal',()=>{refreshProfiles();loadLabels();loadRatios();loadRss();loadSmartQueue();loadRtConfig();loadAutomations();loadCleanup();loadAppStatus();loadPreferences();renderColumnManager();applyColumnVisibility();updateAutomationForm();}); const toolPanelIds={rtorrents:'toolRtorrents',settings:'toolRtorrents',torrentstats:'toolTorrentStats',preferences:'toolPreferences',labels:'toolLabels',ratio:'toolRatio',rss:'toolRss',columns:'toolColumns',smart:'toolSmart',automations:'toolAutomations',rtconfig:'toolRtconfig',cleanup:'toolCleanup',appstatus:'toolAppstatus'}; const hideToolPanels=()=>Object.values(toolPanelIds).filter((v,i,a)=>a.indexOf(v)===i).forEach(id=>$(id)?.classList.add('d-none')); const showToolPanel=tool=>{hideToolPanels(); $(toolPanelIds[tool]||'toolRtorrents')?.classList.remove('d-none');}; const activateToolTab=tool=>{document.querySelectorAll('.tool-tab').forEach(x=>x.classList.toggle('active',(x.dataset.tool||'rtorrents')===tool)); showToolPanel(tool); if(tool==='torrentstats') loadTorrentStats(false); if(tool==='appstatus') loadAppStatus(); if(tool==='cleanup') loadCleanup(); if(tool==='preferences') loadPreferences();}; document.querySelectorAll('.tool-tab').forEach(b=>b.addEventListener('click',()=>activateToolTab(b.dataset.tool||'rtorrents'))); $('torrentStatsRefreshBtn')?.addEventListener('click',()=>loadTorrentStats(true)); $('rssFeedBtn')?.addEventListener('click',async()=>{await post('/api/rss/feeds',{name:$('rssName').value,url:$('rssUrl').value}); loadRss();}); $('rssRuleBtn')?.addEventListener('click',async()=>{await post('/api/rss/rules',{name:$('rssRuleName').value,pattern:$('rssPattern').value,save_path:$('rssPath').value,label:$('rssLabel').value}); loadRss();}); $('rssCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/rss/check',{}); toast(`RSS queued ${j.queued} item(s)`,'success');}catch(e){toast(e.message,'danger');} finally{setBusy(false);}}); $('smartSaveBtn')?.addEventListener('click',saveSmartQueue); $('smartCheckBtn')?.addEventListener('click',async()=>{setBusy(true); try{const j=await post('/api/smart-queue/check',{}); const r=j.result||{}; if(j.torrent_patch) patchRows(j.torrent_patch); const noEffect=r.start_no_effect?.length||0; const requested=r.resume_requested?.length||0; const cap=r.rtorrent_cap?.updated?`, cap ${r.rtorrent_cap.current}->${r.rtorrent_cap.new}`:''; const waiting=r.waiting_labeled||0; const tail=noEffect?`, no effect ${noEffect}`:requested?`, requested ${requested}`:''; const waitTail=waiting?`, waiting labeled ${waiting}`:''; toast(`Smart Queue: paused ${r.paused?.length||0}, resumed ${r.resumed?.length||0}${tail}${waitTail}${cap}`,'success'); await loadSmartQueue();}catch(e){toast(e.message,'danger');}finally{setBusy(false);}}); $('smartManager')?.addEventListener('click',async e=>{const h=e.target.closest('.smart-unexclude')?.dataset.hash; if(!h)return; await post('/api/smart-queue/exclusion',{hash:h,excluded:false}); await loadSmartQueue();}); $('cleanupManager')?.addEventListener('click',async e=>{ if(e.target.closest('#cleanupRefreshBtn')) return loadCleanup(); if(e.target.closest('#cleanupJobsBtn')) return runCleanupAction('/api/cleanup/jobs','Clear finished job logs'); if(e.target.closest('#cleanupSmartQueueBtn')) return runCleanupAction('/api/cleanup/smart-queue','Clear Smart Queue logs'); if(e.target.closest('#cleanupAllBtn')) return runCleanupAction('/api/cleanup/all','Clear job and Smart Queue logs'); }); $('rtConfigReloadBtn')?.addEventListener('click',loadRtConfig); $('rtConfigSaveBtn')?.addEventListener('click',saveRtConfig); $('rtConfigGenerateBtn')?.addEventListener('click',generateRtConfig); $('rtConfigManager')?.addEventListener('input',e=>{ if(e.target.classList.contains('rt-config-input')) updateRtConfigDirty(); }); $('rtConfigManager')?.addEventListener('change',e=>{ if(e.target.classList.contains('rt-config-input')){ const label=e.target.closest('.rt-config-switch')?.querySelector('.form-check-label'); if(label) label.textContent=e.target.checked?'On':'Off'; updateRtConfigDirty(); } }); $('rtConfigApplyOnStart')?.addEventListener('change',updateRtConfigDirty); $('peersRefreshSelect')?.addEventListener('change',async e=>{peersRefreshSeconds=Number(e.target.value||0); await post('/api/preferences',{peers_refresh_seconds:peersRefreshSeconds}).catch(()=>{}); setupPeersRefresh(activeTab()); toast('Peers refresh preference saved','success');});
|
||||||
$('autoConditionType')?.addEventListener('change',updateAutomationForm); $('autoEffectType')?.addEventListener('change',updateAutomationForm); $('automationSaveBtn')?.addEventListener('click',saveAutomation); $('automationCheckBtn')?.addEventListener('click',async()=>{setBusy(true);try{const j=await post('/api/automations/check',{}); toast(`Automations applied ${j.result?.applied?.length||0} item(s)`,'success'); await loadAutomations();}catch(e){toast(e.message,'danger');}finally{setBusy(false);}}); $('automationManager')?.addEventListener('click',async e=>{const id=e.target.closest('.automation-delete')?.dataset.id;if(!id)return;if(!confirm('Delete this automation rule?'))return;const r=await fetch('/api/automations/'+id,{method:'DELETE'});const j=await r.json();if(!j.ok)toast(j.error||'Delete failed','danger');await loadAutomations();});
|
$('autoConditionType')?.addEventListener('change',updateAutomationForm); $('autoEffectType')?.addEventListener('change',updateAutomationForm); $('automationSaveBtn')?.addEventListener('click',saveAutomation); $('automationCheckBtn')?.addEventListener('click',async()=>{setBusy(true);try{const j=await post('/api/automations/check',{}); toast(`Automations applied ${j.result?.applied?.length||0} item(s)`,'success'); await loadAutomations();}catch(e){toast(e.message,'danger');}finally{setBusy(false);}}); $('automationManager')?.addEventListener('click',async e=>{const id=e.target.closest('.automation-delete')?.dataset.id;if(!id)return;if(!confirm('Delete this automation rule?'))return;const r=await fetch('/api/automations/'+id,{method:'DELETE'});const j=await r.json();if(!j.ok)toast(j.error||'Delete failed','danger');await loadAutomations();});
|
||||||
document.addEventListener('click',async e=>{ const btn=e.target.closest('.delete-label'); if(!btn)return; if(!confirm('Delete this label?')) return; setBusy(true); try{ const r=await fetch('/api/labels/'+btn.dataset.id,{method:'DELETE'}); const j=await r.json(); if(!j.ok) throw new Error(j.error||'Delete failed'); await loadLabels(); toast('Label deleted','success'); }catch(err){toast(err.message,'danger');} finally{setBusy(false);} });
|
document.addEventListener('click',async e=>{ const btn=e.target.closest('.delete-label'); if(!btn)return; if(!confirm('Delete this label?')) return; setBusy(true); try{ const r=await fetch('/api/labels/'+btn.dataset.id,{method:'DELETE'}); const j=await r.json(); if(!j.ok) throw new Error(j.error||'Delete failed'); await loadLabels(); toast('Label deleted','success'); }catch(err){toast(err.message,'danger');} finally{setBusy(false);} });
|
||||||
$('bulkClearBtn')?.addEventListener('click',()=>{selected.clear(); selectedHash=null; lastSelectedHash=null; updateBulkBar(); if($('selectAll')) $('selectAll').checked=false; if($('detailPane')) $('detailPane').innerHTML='Select a torrent.'; setupPeersRefresh('general'); scheduleRender(true);});
|
$('bulkClearBtn')?.addEventListener('click',()=>{selected.clear(); selectedHash=null; lastSelectedHash=null; updateBulkBar(); if($('selectAll')) $('selectAll').checked=false; if($('detailPane')) $('detailPane').innerHTML='Select a torrent.'; setupPeersRefresh('general'); scheduleRender(true);});
|
||||||
@@ -841,6 +841,6 @@ ${disk.error}`:''}`;
|
|||||||
b.classList.add("btn-primary"); b.classList.remove("btn-outline-secondary");
|
b.classList.add("btn-primary"); b.classList.remove("btn-outline-secondary");
|
||||||
loadTrafficHistory(b.dataset.range||"7d");
|
loadTrafficHistory(b.dataset.range||"7d");
|
||||||
}));
|
}));
|
||||||
socket.on('connect',()=>{ if(!hasActiveProfile){ showFirstRunSetup(); return; } $('connBadge').className='badge text-bg-success'; $('connBadge').textContent='online'; setInitialLoader('Loading torrents...','Connection is ready. Waiting for the first torrent snapshot.'); socket.emit('select_profile',{profile_id:window.PYTORRENT.activeProfile}); }); socket.on('disconnect',()=>{ $('connBadge').className='badge text-bg-danger'; $('connBadge').textContent='offline'; setInitialLoader('Waiting for connection...','pyTorrent is not connected yet. The application will open after data is received.'); }); socket.io.on('reconnect_attempt',()=>{ $('connBadge').className='badge text-bg-warning'; $('connBadge').textContent='reconnecting'; setInitialLoader('Reconnecting...','Trying to restore the live connection and load torrent data.'); }); socket.io.on('reconnect',()=>{ if(!hasActiveProfile){ showFirstRunSetup(); return; } $('connBadge').className='badge text-bg-success'; $('connBadge').textContent='online'; setInitialLoader('Loading torrents...','Connection restored. Waiting for the first torrent snapshot.'); socket.emit('select_profile',{profile_id:window.PYTORRENT.activeProfile}); }); socket.on('profile_required',()=>showFirstRunSetup()); socket.on('torrent_snapshot',msg=>{hasTorrentSnapshot=true;torrentSummary=msg.summary||null;torrents.clear();(msg.torrents||[]).forEach(t=>torrents.set(t.hash,t));scheduleRender(true);hideInitialLoader();}); socket.on('torrent_patch',patchRows); socket.on('job_update',()=>{ if(document.body.classList.contains('modal-open')) loadJobs().catch(()=>{}); }); socket.on('operation_started',msg=>{setBusy(true);markTorrentOperation(msg.hashes||[],msg.action,msg.job_id,'running');toast(`${msg.action} started`,'secondary');}); socket.on('operation_finished',msg=>{setBusy(false);clearJobOperation(msg.job_id,msg.hashes||[]);toast(`${msg.action} done`,'success');}); socket.on('operation_failed',msg=>{setBusy(false);clearJobOperation(msg.job_id,msg.hashes||[]);toast(`${msg.action}: ${msg.error}`,'danger');}); socket.on('rtorrent_error',msg=>{ if(msg.error){$('connBadge').className='badge badge-degraded';$('connBadge').textContent='degraded'; setInitialLoader('Waiting for rTorrent...','rTorrent is not ready yet. Data will appear automatically after it responds.');} }); socket.on('heartbeat',msg=>{ if(msg.error){$('connBadge').className='badge badge-degraded';$('connBadge').textContent='degraded'; setInitialLoader('Waiting for rTorrent...','rTorrent is not ready yet. Data will appear automatically after it responds.');} else if(socket.connected){$('connBadge').className='badge text-bg-success';$('connBadge').textContent='online';} }); socket.on('smart_queue_update',msg=>{ if(msg && msg.enabled){ const noEffect=msg.start_no_effect?.length||0; const requested=msg.resume_requested?.length||0; const cap=msg.rtorrent_cap?.updated?`, cap ${msg.rtorrent_cap.current}->${msg.rtorrent_cap.new}`:''; const tail=noEffect?`, no effect ${noEffect}`:requested?`, requested ${requested}`:''; toast(`Smart Queue: paused ${msg.paused?.length||0}, resumed ${msg.resumed?.length||0}${tail}${cap}`,'secondary'); } }); socket.on('automation_update',msg=>{ if(msg?.applied?.length) toast(`Automations applied ${msg.applied.length} item(s)`,'secondary'); }); socket.on('torrent_stats_update',msg=>{ if(msg?.stats){ renderTorrentStats(msg.stats); } else if(msg?.error && $('toolTorrentStats') && !$('toolTorrentStats').classList.contains('d-none')){ toast(`Torrent stats: ${msg.error}`,'danger'); } }); socket.on('rtorrent_config_applied',msg=>{ if(msg?.result?.updated?.length) toast(`Startup rTorrent config applied (${msg.result.updated.length})`,'success'); if(msg?.error) toast(`Startup rTorrent config: ${msg.error}`,'danger'); }); socket.on('system_stats',s=>{ const usageAvailable=s.usage_available!==false && s.cpu!==undefined && s.ram!==undefined; $('statCpuBox')?.classList.toggle('d-none',!usageAvailable);$('statRamBox')?.classList.toggle('d-none',!usageAvailable);$('systemChart')?.classList.toggle('d-none',!usageAvailable); if(usageAvailable){$('statCpu').textContent=s.cpu??'-';$('statRam').textContent=s.ram??'-';drawSystemUsage(s.cpu,s.ram);} $('statVersion').textContent=s.version||'-';$('statDl').textContent=s.down_rate_h||'0 B/s';$('statUl').textContent=s.up_rate_h||'0 B/s';if($('mobileSpeedDl')) $('mobileSpeedDl').textContent=s.down_rate_h||'0 B/s';if($('mobileSpeedUl')) $('mobileSpeedUl').textContent=s.up_rate_h||'0 B/s';lastLimits={down:Number(s.down_limit||0),up:Number(s.up_limit||0)};$('statDlLimit').textContent=s.down_limit_h||'∞';$('statUlLimit').textContent=s.up_limit_h||'∞';$('statTotalDl').textContent=compactTransferText(s.total_down_h);$('statTotalUl').textContent=compactTransferText(s.total_up_h);drawTraffic(s.down_rate,s.up_rate);drawDiskUsage(s.disk);updateSocketStatus(s);applyFooterPreferences();});
|
socket.on('connect',()=>{ if(!hasActiveProfile){ showFirstRunSetup(); return; } $('connBadge').className='badge text-bg-success'; $('connBadge').textContent='online'; setInitialLoader('Loading torrents...','Connection is ready. Waiting for the first torrent snapshot.'); socket.emit('select_profile',{profile_id:window.PYTORRENT.activeProfile}); }); socket.on('disconnect',()=>{ $('connBadge').className='badge text-bg-danger'; $('connBadge').textContent='offline'; setInitialLoader('Waiting for connection...','pyTorrent is not connected yet. The application will open after data is received.'); }); socket.io.on('reconnect_attempt',()=>{ $('connBadge').className='badge text-bg-warning'; $('connBadge').textContent='reconnecting'; setInitialLoader('Reconnecting...','Trying to restore the live connection and load torrent data.'); }); socket.io.on('reconnect',()=>{ if(!hasActiveProfile){ showFirstRunSetup(); return; } $('connBadge').className='badge text-bg-success'; $('connBadge').textContent='online'; setInitialLoader('Loading torrents...','Connection restored. Waiting for the first torrent snapshot.'); socket.emit('select_profile',{profile_id:window.PYTORRENT.activeProfile}); }); socket.on('profile_required',()=>showFirstRunSetup()); socket.on('torrent_snapshot',msg=>{hasTorrentSnapshot=true;torrentSummary=msg.summary||null;torrents.clear();(msg.torrents||[]).forEach(t=>torrents.set(t.hash,t));scheduleRender(true);hideInitialLoader();}); socket.on('torrent_patch',patchRows); socket.on('job_update',()=>{ if(document.body.classList.contains('modal-open')) loadJobs().catch(()=>{}); }); socket.on('operation_started',msg=>{setBusy(true);markTorrentOperation(msg.hashes||[],msg.action,msg.job_id,'running');toast(`${msg.action} started`,'secondary');}); socket.on('operation_finished',msg=>{setBusy(false);clearJobOperation(msg.job_id,msg.hashes||[]);toast(`${msg.action} done`,'success');}); socket.on('operation_failed',msg=>{setBusy(false);clearJobOperation(msg.job_id,msg.hashes||[]);toast(`${msg.action}: ${msg.error}`,'danger');}); socket.on('rtorrent_error',msg=>{ if(msg.error){$('connBadge').className='badge badge-degraded';$('connBadge').textContent='degraded'; setInitialLoader('Waiting for rTorrent...','rTorrent is not ready yet. Data will appear automatically after it responds.');} }); socket.on('heartbeat',msg=>{ if(msg.error){$('connBadge').className='badge badge-degraded';$('connBadge').textContent='degraded'; setInitialLoader('Waiting for rTorrent...','rTorrent is not ready yet. Data will appear automatically after it responds.');} else if(socket.connected){$('connBadge').className='badge text-bg-success';$('connBadge').textContent='online';} }); socket.on('smart_queue_update',msg=>{ if(msg && msg.enabled){ const noEffect=msg.start_no_effect?.length||0; const requested=msg.resume_requested?.length||0; const cap=msg.rtorrent_cap?.updated?`, cap ${msg.rtorrent_cap.current}->${msg.rtorrent_cap.new}`:''; const waiting=msg.waiting_labeled||0; const tail=noEffect?`, no effect ${noEffect}`:requested?`, requested ${requested}`:''; const waitTail=waiting?`, waiting labeled ${waiting}`:''; toast(`Smart Queue: paused ${msg.paused?.length||0}, resumed ${msg.resumed?.length||0}${tail}${waitTail}${cap}`,'secondary'); } }); socket.on('automation_update',msg=>{ if(msg?.applied?.length) toast(`Automations applied ${msg.applied.length} item(s)`,'secondary'); }); socket.on('torrent_stats_update',msg=>{ if(msg?.stats){ renderTorrentStats(msg.stats); } else if(msg?.error && $('toolTorrentStats') && !$('toolTorrentStats').classList.contains('d-none')){ toast(`Torrent stats: ${msg.error}`,'danger'); } }); socket.on('rtorrent_config_applied',msg=>{ if(msg?.result?.updated?.length) toast(`Startup rTorrent config applied (${msg.result.updated.length})`,'success'); if(msg?.error) toast(`Startup rTorrent config: ${msg.error}`,'danger'); }); socket.on('system_stats',s=>{ const usageAvailable=s.usage_available!==false && s.cpu!==undefined && s.ram!==undefined; $('statCpuBox')?.classList.toggle('d-none',!usageAvailable);$('statRamBox')?.classList.toggle('d-none',!usageAvailable);$('systemChart')?.classList.toggle('d-none',!usageAvailable); if(usageAvailable){$('statCpu').textContent=s.cpu??'-';$('statRam').textContent=s.ram??'-';drawSystemUsage(s.cpu,s.ram);} $('statVersion').textContent=s.version||'-';$('statDl').textContent=s.down_rate_h||'0 B/s';$('statUl').textContent=s.up_rate_h||'0 B/s';if($('mobileSpeedDl')) $('mobileSpeedDl').textContent=s.down_rate_h||'0 B/s';if($('mobileSpeedUl')) $('mobileSpeedUl').textContent=s.up_rate_h||'0 B/s';lastLimits={down:Number(s.down_limit||0),up:Number(s.up_limit||0)};$('statDlLimit').textContent=s.down_limit_h||'∞';$('statUlLimit').textContent=s.up_limit_h||'∞';$('statTotalDl').textContent=compactTransferText(s.total_down_h);$('statTotalUl').textContent=compactTransferText(s.total_up_h);drawTraffic(s.down_rate,s.up_rate);drawDiskUsage(s.disk);updateSocketStatus(s);applyFooterPreferences();});
|
||||||
updateSortHeaders(); applyColumnVisibility(); renderColumnManager(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{});
|
updateSortHeaders(); applyColumnVisibility(); renderColumnManager(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user