fix in queue
This commit is contained in:
@@ -126,6 +126,7 @@ CREATE TABLE IF NOT EXISTS smart_queue_settings (
|
|||||||
stalled_seconds INTEGER DEFAULT 300,
|
stalled_seconds INTEGER DEFAULT 300,
|
||||||
min_speed_bytes INTEGER DEFAULT 1024,
|
min_speed_bytes INTEGER DEFAULT 1024,
|
||||||
min_seeds INTEGER DEFAULT 1,
|
min_seeds INTEGER DEFAULT 1,
|
||||||
|
manage_stopped INTEGER DEFAULT 0,
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
PRIMARY KEY(user_id, profile_id)
|
PRIMARY KEY(user_id, profile_id)
|
||||||
);
|
);
|
||||||
@@ -262,6 +263,7 @@ MIGRATIONS = [
|
|||||||
"ALTER TABLE rtorrent_config_overrides ADD COLUMN apply_on_start INTEGER DEFAULT 0",
|
"ALTER TABLE rtorrent_config_overrides ADD COLUMN apply_on_start INTEGER DEFAULT 0",
|
||||||
"ALTER TABLE rtorrent_config_overrides ADD COLUMN baseline_value TEXT",
|
"ALTER TABLE rtorrent_config_overrides ADD COLUMN baseline_value TEXT",
|
||||||
"ALTER TABLE torrent_stats_cache ADD COLUMN updated_epoch REAL DEFAULT 0",
|
"ALTER TABLE torrent_stats_cache ADD COLUMN updated_epoch REAL DEFAULT 0",
|
||||||
|
"ALTER TABLE smart_queue_settings ADD COLUMN manage_stopped INTEGER DEFAULT 0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ def _default_settings(user_id: int, profile_id: int) -> dict[str, Any]:
|
|||||||
'stalled_seconds': 300,
|
'stalled_seconds': 300,
|
||||||
'min_speed_bytes': 1024,
|
'min_speed_bytes': 1024,
|
||||||
'min_seeds': 1,
|
'min_seeds': 1,
|
||||||
|
'manage_stopped': 0,
|
||||||
'updated_at': utcnow(),
|
'updated_at': utcnow(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,20 +53,23 @@ def save_settings(profile_id: int, data: dict[str, Any], user_id: int | None = N
|
|||||||
'stalled_seconds': max(30, int(data.get('stalled_seconds') or current.get('stalled_seconds') or 300)),
|
'stalled_seconds': max(30, int(data.get('stalled_seconds') or current.get('stalled_seconds') or 300)),
|
||||||
'min_speed_bytes': max(0, int(data.get('min_speed_bytes') or current.get('min_speed_bytes') or 0)),
|
'min_speed_bytes': max(0, int(data.get('min_speed_bytes') or current.get('min_speed_bytes') or 0)),
|
||||||
'min_seeds': max(0, int(data.get('min_seeds') or current.get('min_seeds') or 0)),
|
'min_seeds': max(0, int(data.get('min_seeds') or current.get('min_seeds') or 0)),
|
||||||
|
# Note: Switch chroni całkiem zatrzymane torrenty przed automatycznym startem; domyślnie Smart Queue zarządza tylko paused.
|
||||||
|
'manage_stopped': 1 if data.get('manage_stopped', current.get('manage_stopped')) else 0,
|
||||||
}
|
}
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
'''INSERT INTO smart_queue_settings(user_id,profile_id,enabled,max_active_downloads,stalled_seconds,min_speed_bytes,min_seeds,updated_at)
|
'''INSERT INTO smart_queue_settings(user_id,profile_id,enabled,max_active_downloads,stalled_seconds,min_speed_bytes,min_seeds,manage_stopped,updated_at)
|
||||||
VALUES(?,?,?,?,?,?,?,?)
|
VALUES(?,?,?,?,?,?,?,?,?)
|
||||||
ON CONFLICT(user_id, profile_id) DO UPDATE SET
|
ON CONFLICT(user_id, profile_id) DO UPDATE SET
|
||||||
enabled=excluded.enabled,
|
enabled=excluded.enabled,
|
||||||
max_active_downloads=excluded.max_active_downloads,
|
max_active_downloads=excluded.max_active_downloads,
|
||||||
stalled_seconds=excluded.stalled_seconds,
|
stalled_seconds=excluded.stalled_seconds,
|
||||||
min_speed_bytes=excluded.min_speed_bytes,
|
min_speed_bytes=excluded.min_speed_bytes,
|
||||||
min_seeds=excluded.min_seeds,
|
min_seeds=excluded.min_seeds,
|
||||||
|
manage_stopped=excluded.manage_stopped,
|
||||||
updated_at=excluded.updated_at''',
|
updated_at=excluded.updated_at''',
|
||||||
(user_id, profile_id, settings['enabled'], settings['max_active_downloads'], settings['stalled_seconds'], settings['min_speed_bytes'], settings['min_seeds'], now),
|
(user_id, profile_id, settings['enabled'], settings['max_active_downloads'], settings['stalled_seconds'], settings['min_speed_bytes'], settings['min_seeds'], settings['manage_stopped'], now),
|
||||||
)
|
)
|
||||||
return get_settings(profile_id, user_id)
|
return get_settings(profile_id, user_id)
|
||||||
|
|
||||||
@@ -195,10 +199,15 @@ def _mark_auto_paused(client: Any, profile_id: int, torrent: dict[str, Any]) ->
|
|||||||
return _set_smart_queue_label(client, torrent_hash)
|
return _set_smart_queue_label(client, torrent_hash)
|
||||||
|
|
||||||
|
|
||||||
def _is_smart_queue_hold(torrent: dict[str, Any] | None) -> bool:
|
def _is_smart_queue_hold(torrent: dict[str, Any] | None, manage_stopped: bool = True) -> bool:
|
||||||
if not torrent or int(torrent.get('complete') or 0):
|
if not torrent or int(torrent.get('complete') or 0):
|
||||||
return False
|
return False
|
||||||
return bool(torrent.get('paused')) or not int(torrent.get('active') or 0) or not int(torrent.get('state') or 0)
|
# Note: Gdy manage_stopped=False, techniczne labele Smart Queue dotyczą tylko paused, a nie całkiem zatrzymanych torrentów.
|
||||||
|
if bool(torrent.get('paused')):
|
||||||
|
return True
|
||||||
|
if not manage_stopped and not int(torrent.get('state') or 0):
|
||||||
|
return False
|
||||||
|
return not int(torrent.get('active') or 0) or not int(torrent.get('state') or 0)
|
||||||
|
|
||||||
|
|
||||||
def _clear_untracked_smart_queue_label(client: Any, torrent_hash: str, current_label: str) -> bool:
|
def _clear_untracked_smart_queue_label(client: Any, torrent_hash: str, current_label: str) -> bool:
|
||||||
@@ -212,7 +221,7 @@ def _clear_untracked_smart_queue_label(client: Any, torrent_hash: str, current_l
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str, Any]], keep_hashes: set[str]) -> list[str]:
|
def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str, Any]], keep_hashes: set[str], manage_stopped: bool = True) -> list[str]:
|
||||||
by_hash = {str(t.get('hash') or ''): t for t in torrents}
|
by_hash = {str(t.get('hash') or ''): t for t in torrents}
|
||||||
restored: list[str] = []
|
restored: list[str] = []
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
@@ -225,7 +234,7 @@ def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str,
|
|||||||
if not h or h in keep_hashes:
|
if not h or h in keep_hashes:
|
||||||
continue
|
continue
|
||||||
current_label = '' if t is None else str(t.get('label') or '')
|
current_label = '' if t is None else str(t.get('label') or '')
|
||||||
if not _is_smart_queue_hold(t):
|
if not _is_smart_queue_hold(t, manage_stopped):
|
||||||
if _restore_auto_label(client, profile_id, h, None if t is None else current_label):
|
if _restore_auto_label(client, profile_id, h, None if t is None else current_label):
|
||||||
restored.append(h)
|
restored.append(h)
|
||||||
continue
|
continue
|
||||||
@@ -233,7 +242,7 @@ def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str,
|
|||||||
_set_smart_queue_label(client, h)
|
_set_smart_queue_label(client, h)
|
||||||
|
|
||||||
for h, t in by_hash.items():
|
for h, t in by_hash.items():
|
||||||
if not h or h in keep_hashes or h in tracked_hashes or _is_smart_queue_hold(t):
|
if not h or h in keep_hashes or h in tracked_hashes or _is_smart_queue_hold(t, manage_stopped):
|
||||||
continue
|
continue
|
||||||
if _clear_untracked_smart_queue_label(client, h, str(t.get('label') or '')):
|
if _clear_untracked_smart_queue_label(client, h, str(t.get('label') or '')):
|
||||||
restored.append(h)
|
restored.append(h)
|
||||||
@@ -252,7 +261,7 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
try:
|
try:
|
||||||
# Note: Przy wyłączonym Smart Queue sprzątamy wyłącznie techniczne labele, bez startowania lub pauzowania torrentów.
|
# Note: Przy wyłączonym Smart Queue sprzątamy wyłącznie techniczne labele, bez startowania lub pauzowania torrentów.
|
||||||
torrents = rtorrent.list_torrents(profile)
|
torrents = rtorrent.list_torrents(profile)
|
||||||
restored = _cleanup_auto_labels(rtorrent.client_for(profile), profile_id, torrents, set())
|
restored = _cleanup_auto_labels(rtorrent.client_for(profile), profile_id, torrents, set(), bool(settings.get('manage_stopped')))
|
||||||
except Exception:
|
except Exception:
|
||||||
restored = []
|
restored = []
|
||||||
add_history(profile_id, 'skipped_disabled', [], [], 0, {'enabled': False, 'labels_restored': restored}, user_id)
|
add_history(profile_id, 'skipped_disabled', [], [], 0, {'enabled': False, 'labels_restored': restored}, user_id)
|
||||||
@@ -260,8 +269,15 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
|
|
||||||
torrents = rtorrent.list_torrents(profile)
|
torrents = rtorrent.list_torrents(profile)
|
||||||
excluded = _excluded_hashes(profile_id, user_id)
|
excluded = _excluded_hashes(profile_id, user_id)
|
||||||
|
manage_stopped = bool(settings.get('manage_stopped'))
|
||||||
downloading = [t for t in torrents if not int(t.get('complete') or 0) and int(t.get('state') or 0) and not t.get('paused') and t.get('hash') not in excluded]
|
downloading = [t for t in torrents if not int(t.get('complete') or 0) and int(t.get('state') or 0) and not t.get('paused') and t.get('hash') not in excluded]
|
||||||
stopped = [t for t in torrents if not int(t.get('complete') or 0) and (not int(t.get('state') or 0) or t.get('paused')) and t.get('hash') not in excluded]
|
# Note: Domyślnie kolejka wznawia wyłącznie paused; stopped są kandydatami tylko po włączeniu switcha w ustawieniach.
|
||||||
|
stopped = [
|
||||||
|
t for t in torrents
|
||||||
|
if not int(t.get('complete') or 0)
|
||||||
|
and t.get('hash') not in excluded
|
||||||
|
and (bool(t.get('paused')) or (manage_stopped and not int(t.get('state') or 0)))
|
||||||
|
]
|
||||||
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)
|
||||||
stalled_seconds = int(settings.get('stalled_seconds') or 300)
|
stalled_seconds = int(settings.get('stalled_seconds') or 300)
|
||||||
@@ -327,6 +343,7 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
paused: list[str] = []
|
paused: list[str] = []
|
||||||
resumed: list[str] = []
|
resumed: list[str] = []
|
||||||
label_failed: list[str] = []
|
label_failed: list[str] = []
|
||||||
|
start_failed: list[dict[str, str]] = []
|
||||||
for t in to_pause:
|
for t in to_pause:
|
||||||
try:
|
try:
|
||||||
c.call('d.pause', t['hash'])
|
c.call('d.pause', t['hash'])
|
||||||
@@ -336,15 +353,17 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
for t in to_resume:
|
for t in to_resume:
|
||||||
|
h = t['hash']
|
||||||
try:
|
try:
|
||||||
# Note: Wznowienie usuwa techniczny label przed startem i ponawia sprzątanie po starcie, gdy cache rTorrent jest opóźniony.
|
# Note: Paused wymaga resume+start, a stopped startujemy bez wcześniejszego resume tylko wtedy, gdy switch na to pozwala.
|
||||||
_restore_auto_label(c, profile_id, t['hash'], str(t.get('label') or ''))
|
_restore_auto_label(c, profile_id, h, str(t.get('label') or ''))
|
||||||
c.call('d.resume', t['hash'])
|
if bool(t.get('paused')):
|
||||||
c.call('d.start', t['hash'])
|
c.call('d.resume', h)
|
||||||
_restore_auto_label(c, profile_id, t['hash'], None)
|
c.call('d.start', h)
|
||||||
resumed.append(t['hash'])
|
_restore_auto_label(c, profile_id, h, None)
|
||||||
except Exception:
|
resumed.append(h)
|
||||||
pass
|
except Exception as exc:
|
||||||
restored = _cleanup_auto_labels(c, profile_id, torrents, set(paused))
|
start_failed.append({'hash': h, 'error': str(exc)})
|
||||||
add_history(profile_id, 'force_check' if force else 'auto_check', paused, resumed, len(torrents), {'excluded': len(excluded), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'labels_restored': restored, 'labels_failed': label_failed, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after': active_after_pause + len(resumed)}, user_id)
|
restored = _cleanup_auto_labels(c, profile_id, torrents, set(paused), manage_stopped)
|
||||||
return {'ok': True, 'enabled': bool(settings.get('enabled')), 'paused': paused, 'resumed': resumed, 'labels_restored': restored, 'labels_failed': label_failed, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings}
|
add_history(profile_id, 'force_check' if force else 'auto_check', paused, resumed, len(torrents), {'excluded': len(excluded), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'manage_stopped': manage_stopped, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after': active_after_pause + len(resumed)}, user_id)
|
||||||
|
return {'ok': True, 'enabled': bool(settings.get('enabled')), 'paused': paused, 'resumed': resumed, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import threading
|
||||||
import psutil
|
import psutil
|
||||||
from flask_socketio import emit
|
from flask_socketio import emit
|
||||||
from ..config import POLL_INTERVAL
|
from ..config import POLL_INTERVAL
|
||||||
@@ -9,10 +10,10 @@ from .torrent_summary import cached_summary
|
|||||||
from . import rtorrent, smart_queue, traffic_history, automation_rules, torrent_stats
|
from . import rtorrent, smart_queue, traffic_history, automation_rules, torrent_stats
|
||||||
|
|
||||||
_started = False
|
_started = False
|
||||||
|
_start_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def register_socketio_handlers(socketio):
|
def register_socketio_handlers(socketio):
|
||||||
global _started
|
|
||||||
|
|
||||||
def poller():
|
def poller():
|
||||||
tick = 0
|
tick = 0
|
||||||
@@ -63,12 +64,19 @@ def register_socketio_handlers(socketio):
|
|||||||
tick += 1
|
tick += 1
|
||||||
socketio.sleep(POLL_INTERVAL)
|
socketio.sleep(POLL_INTERVAL)
|
||||||
|
|
||||||
@socketio.on("connect")
|
def ensure_poller_started():
|
||||||
def handle_connect():
|
|
||||||
global _started
|
global _started
|
||||||
|
with _start_lock:
|
||||||
if not _started:
|
if not _started:
|
||||||
|
# Note: Poller startuje przy starcie aplikacji, więc Smart Queue i automatyzacje działają bez otwartego UI.
|
||||||
socketio.start_background_task(poller)
|
socketio.start_background_task(poller)
|
||||||
_started = True
|
_started = True
|
||||||
|
|
||||||
|
ensure_poller_started()
|
||||||
|
|
||||||
|
@socketio.on("connect")
|
||||||
|
def handle_connect():
|
||||||
|
ensure_poller_started()
|
||||||
profile = active_profile()
|
profile = active_profile()
|
||||||
emit("connected", {"ok": True, "profile": profile})
|
emit("connected", {"ok": True, "profile": profile})
|
||||||
if not profile:
|
if not profile:
|
||||||
|
|||||||
@@ -393,9 +393,9 @@
|
|||||||
$('ratioAssignModal')?.addEventListener('show.bs.modal',loadRatios); $('applyRatioBtn')?.addEventListener('click',async()=>{ await runAction('set_ratio_group',{ratio_group:$('ratioAssignSelect').value}); bootstrap.Modal.getInstance($('ratioAssignModal'))?.hide(); }); $('ratioSaveBtn')?.addEventListener('click',async()=>{ await post('/api/ratio-groups',{name:$('ratioName').value,min_ratio:$('ratioMin').value,max_ratio:$('ratioMax').value,seed_time_minutes:$('ratioSeed').value,action:$('ratioAction').value}); loadRatios(); });
|
$('ratioAssignModal')?.addEventListener('show.bs.modal',loadRatios); $('applyRatioBtn')?.addEventListener('click',async()=>{ await runAction('set_ratio_group',{ratio_group:$('ratioAssignSelect').value}); bootstrap.Modal.getInstance($('ratioAssignModal'))?.hide(); }); $('ratioSaveBtn')?.addEventListener('click',async()=>{ await post('/api/ratio-groups',{name:$('ratioName').value,min_ratio:$('ratioMin').value,max_ratio:$('ratioMax').value,seed_time_minutes:$('ratioSeed').value,action:$('ratioAction').value}); loadRatios(); });
|
||||||
async function loadRss(){ const j=await (await fetch('/api/rss')).json(); const feeds=j.feeds||[], rules=j.rules||[]; if($('rssManager')) $('rssManager').innerHTML=`<h6>Feeds</h6>${table(['Name','URL','Last error'],feeds.map(f=>[esc(f.name),esc(f.url),esc(f.last_error||'')]))}<h6 class="mt-3">Rules</h6>${table(['Name','Pattern','Path','Label'],rules.map(r=>[esc(r.name),esc(r.pattern),esc(r.save_path),esc(r.label)]))}`; }
|
async function loadRss(){ const j=await (await fetch('/api/rss')).json(); const feeds=j.feeds||[], rules=j.rules||[]; if($('rssManager')) $('rssManager').innerHTML=`<h6>Feeds</h6>${table(['Name','URL','Last error'],feeds.map(f=>[esc(f.name),esc(f.url),esc(f.last_error||'')]))}<h6 class="mt-3">Rules</h6>${table(['Name','Pattern','Path','Label'],rules.map(r=>[esc(r.name),esc(r.pattern),esc(r.save_path),esc(r.label)]))}`; }
|
||||||
|
|
||||||
async function loadSmartQueue(){ if($('smartManager')) $('smartManager').innerHTML=loadingMarkup('Loading Smart Queue...'); if($('smartHistory')) $('smartHistory').innerHTML=loadingMarkup('Loading Smart Queue history...'); const historyLimit=smartHistoryExpanded?100:10; const j=await (await fetch(`/api/smart-queue?history_limit=${historyLimit}`)).json(); if(!j.ok) return; const st=j.settings||{}, ex=j.exclusions||[], hist=j.history||[]; const totalHistory=Number(j.history_total ?? hist.length); if($('smartEnabled')) $('smartEnabled').checked=!!st.enabled; if($('smartMaxActive')) $('smartMaxActive').value=st.max_active_downloads||5; if($('smartStalled')) $('smartStalled').value=st.stalled_seconds||300; if($('smartMinSpeed')) $('smartMinSpeed').value=Math.round((st.min_speed_bytes||0)/1024); if($('smartMinSeeds')) $('smartMinSeeds').value=st.min_seeds||1; if($('smartManager')) $('smartManager').innerHTML=ex.length?table(['Hash','Reason','Created','Action'],ex.map(x=>[esc(x.torrent_hash),esc(x.reason||''),dateCell(x.created_at),`<button class="btn btn-xs btn-outline-danger smart-unexclude" data-hash="${esc(x.torrent_hash)}"><i class="fa-solid fa-xmark"></i> remove exception</button>`])):'<div class="empty-mini"><i class="fa-solid fa-circle-info"></i> No Smart Queue exceptions. Select torrents and use <b>Exclude selected</b> to keep them outside the queue.</div>'; if($('smartHistory')) { const body=hist.length?table(['Time','Event','Checked','Paused','Resumed'],hist.map(h=>[dateCell(h.created_at),esc(h.event),esc(h.checked_count||0),esc(h.paused_count||0),esc(h.resumed_count||0)])):'<div class="empty-mini">No Smart Queue operations yet.</div>'; const canToggle=totalHistory>10; const toggle=canToggle?`<button id="smartHistoryToggle" class="btn btn-xs btn-outline-secondary mt-2">${smartHistoryExpanded?'Show last 10':'Show more'} (${esc(totalHistory)})</button>`:''; $('smartHistory').innerHTML=`${body}${toggle}`; } }
|
async function loadSmartQueue(){ if($('smartManager')) $('smartManager').innerHTML=loadingMarkup('Loading Smart Queue...'); if($('smartHistory')) $('smartHistory').innerHTML=loadingMarkup('Loading Smart Queue history...'); const historyLimit=smartHistoryExpanded?100:10; const j=await (await fetch(`/api/smart-queue?history_limit=${historyLimit}`)).json(); if(!j.ok) return; const st=j.settings||{}, ex=j.exclusions||[], hist=j.history||[]; const totalHistory=Number(j.history_total ?? hist.length); if($('smartEnabled')) $('smartEnabled').checked=!!st.enabled; if($('smartManageStopped')) $('smartManageStopped').checked=!!st.manage_stopped; if($('smartMaxActive')) $('smartMaxActive').value=st.max_active_downloads||5; if($('smartStalled')) $('smartStalled').value=st.stalled_seconds||300; if($('smartMinSpeed')) $('smartMinSpeed').value=Math.round((st.min_speed_bytes||0)/1024); if($('smartMinSeeds')) $('smartMinSeeds').value=st.min_seeds||1; if($('smartManager')) $('smartManager').innerHTML=ex.length?table(['Hash','Reason','Created','Action'],ex.map(x=>[esc(x.torrent_hash),esc(x.reason||''),dateCell(x.created_at),`<button class="btn btn-xs btn-outline-danger smart-unexclude" data-hash="${esc(x.torrent_hash)}"><i class="fa-solid fa-xmark"></i> remove exception</button>`])):'<div class="empty-mini"><i class="fa-solid fa-circle-info"></i> No Smart Queue exceptions. Select torrents and use <b>Exclude selected</b> to keep them outside the queue.</div>'; if($('smartHistory')) { const body=hist.length?table(['Time','Event','Checked','Paused','Resumed'],hist.map(h=>[dateCell(h.created_at),esc(h.event),esc(h.checked_count||0),esc(h.paused_count||0),esc(h.resumed_count||0)])):'<div class="empty-mini">No Smart Queue operations yet.</div>'; const canToggle=totalHistory>10; const toggle=canToggle?`<button id="smartHistoryToggle" class="btn btn-xs btn-outline-secondary mt-2">${smartHistoryExpanded?'Show last 10':'Show more'} (${esc(totalHistory)})</button>`:''; $('smartHistory').innerHTML=`${body}${toggle}`; } }
|
||||||
async function setSmartException(hashes, excluded, reason='manual'){ const list=[...new Set(hashes||[])].filter(Boolean); if(!list.length) return toast('No torrents selected','warning'); setBusy(true); try{ for(const h of list) await post('/api/smart-queue/exclusion',{hash:h,excluded,reason}); toast(excluded?'Smart Queue exception added':'Smart Queue exception removed','success'); await loadSmartQueue(); }catch(e){toast(e.message,'danger');} finally{setBusy(false);} }
|
async function setSmartException(hashes, excluded, reason='manual'){ const list=[...new Set(hashes||[])].filter(Boolean); if(!list.length) return toast('No torrents selected','warning'); setBusy(true); try{ for(const h of list) await post('/api/smart-queue/exclusion',{hash:h,excluded,reason}); toast(excluded?'Smart Queue exception added':'Smart Queue exception removed','success'); await loadSmartQueue(); }catch(e){toast(e.message,'danger');} finally{setBusy(false);} }
|
||||||
async function saveSmartQueue(){ await post('/api/smart-queue',{enabled:$('smartEnabled')?.checked,max_active_downloads:$('smartMaxActive')?.value,stalled_seconds:$('smartStalled')?.value,min_speed_bytes:Math.round(Number($('smartMinSpeed')?.value||0)*1024),min_seeds:$('smartMinSeeds')?.value}); toast('Smart Queue saved','success'); await loadSmartQueue(); }
|
async function saveSmartQueue(){ await post('/api/smart-queue',{enabled:$('smartEnabled')?.checked,manage_stopped:$('smartManageStopped')?.checked,max_active_downloads:$('smartMaxActive')?.value,stalled_seconds:$('smartStalled')?.value,min_speed_bytes:Math.round(Number($('smartMinSpeed')?.value||0)*1024),min_seeds:$('smartMinSeeds')?.value}); toast('Smart Queue saved','success'); await loadSmartQueue(); }
|
||||||
function normalizeRtConfigValue(value, type='text'){
|
function normalizeRtConfigValue(value, type='text'){
|
||||||
const raw=String(value ?? '').trim();
|
const raw=String(value ?? '').trim();
|
||||||
if(type==='bool') return ['1','true','yes','on'].includes(raw.toLowerCase()) ? '1' : '0';
|
if(type==='bool') return ['1','true','yes','on'].includes(raw.toLowerCase()) ? '1' : '0';
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user