From 68d8ddc8d7d627488d58fc66366aaf16a0753134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 31 May 2026 23:09:24 +0200 Subject: [PATCH] surge refill --- pytorrent/__init__.py | 19 +- pytorrent/openapi/openapi.json | 46 ++++ pytorrent/routes/system.py | 10 + pytorrent/services/frontend_assets.py | 33 +++ pytorrent/static/js/app.js | 284 +++++++++--------------- pytorrent/static/js/bootstrapRuntime.js | 2 +- pytorrent/static/styles.css | 17 +- pytorrent/templates/index.html | 4 +- 8 files changed, 217 insertions(+), 198 deletions(-) diff --git a/pytorrent/__init__.py b/pytorrent/__init__.py index 337f165..2249343 100644 --- a/pytorrent/__init__.py +++ b/pytorrent/__init__.py @@ -16,11 +16,9 @@ from .config import ( SOCKETIO_CORS_ALLOWED_ORIGINS, ) from .db import init_db -from .services.frontend_assets import asset_path, bootstrap_css_path, validate_offline_assets -from .utils import file_md5 +from .services.frontend_assets import asset_path, bootstrap_css_path, static_hash, validate_offline_assets socketio = SocketIO(cors_allowed_origins=SOCKETIO_CORS_ALLOWED_ORIGINS, ping_timeout=30, async_mode="threading") -_static_md5_cache: dict[tuple, str] = {} def _wants_json_response() -> bool: @@ -78,17 +76,15 @@ def create_app() -> Flask: @app.context_processor def static_helpers(): + def current_static_hash() -> str: + return static_hash(Path(app.static_folder or "")) + def static_url(filename: str) -> str: path = Path(app.static_folder or "") / filename try: - stat = path.stat() - key = (filename, stat.st_mtime_ns, stat.st_size) - version = _static_md5_cache.get(key) - if not version: - _static_md5_cache.clear() - version = file_md5(path) - _static_md5_cache[key] = version - return url_for("static", filename=filename, v=version) + path.stat() + # Note: A single static hash keeps module imports, CSS and local libraries on the same cache version. + return url_for("static", filename=filename, v=current_static_hash()) except OSError: return url_for("static", filename=filename) @@ -104,6 +100,7 @@ def create_app() -> Flask: "static_url": static_url, "frontend_asset_url": frontend_asset_url, "bootstrap_theme_url": bootstrap_theme_url, + "static_hash": current_static_hash, } @app.after_request diff --git a/pytorrent/openapi/openapi.json b/pytorrent/openapi/openapi.json index 32e810c..9cb2d28 100644 --- a/pytorrent/openapi/openapi.json +++ b/pytorrent/openapi/openapi.json @@ -1359,6 +1359,9 @@ }, "settings": { "$ref": "#/components/schemas/SmartQueueSettings" + }, + "surge_refill_remaining_seconds": { + "type": "integer" } }, "type": "object" @@ -1422,6 +1425,17 @@ "stop_batch_size": { "minimum": 1, "type": "integer" + }, + "surge_refill_enabled": { + "type": "boolean" + }, + "surge_refill_interval_minutes": { + "type": "integer", + "minimum": 1 + }, + "surge_refill_batch_size": { + "type": "integer", + "minimum": 1 } }, "type": "object" @@ -7479,6 +7493,38 @@ } } } + }, + "/api/static_hash": { + "get": { + "tags": [ + "System" + ], + "summary": "Get static asset version hash", + "description": "Returns the current hash for app static assets. Clients can compare it with window.PYTORRENT.staticHash and reload when it changes.", + "responses": { + "200": { + "description": "Static asset hash", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "static_hash": { + "type": "string" + }, + "version": { + "type": "string" + } + } + } + } + } + } + } + } } } } diff --git a/pytorrent/routes/system.py b/pytorrent/routes/system.py index 66a81bf..93994da 100644 --- a/pytorrent/routes/system.py +++ b/pytorrent/routes/system.py @@ -1,7 +1,10 @@ from __future__ import annotations from ._shared import * +from flask import current_app +from pathlib import Path from ..services import operation_logs +from ..services.frontend_assets import static_hash @bp.get("/system/disk") def system_disk(): @@ -46,6 +49,13 @@ def system_status(): +@bp.get("/static_hash") +def static_hash_get(): + # Note: The frontend uses this lightweight version to detect changed static assets on mobile browsers. + value = static_hash(Path(current_app.static_folder or "")) + return ok({"static_hash": value, "version": value}) + + @bp.get("/health") def health_check(): # Note: Lightweight health endpoint avoids rTorrent calls, making it safe for frequent monitoring. diff --git a/pytorrent/services/frontend_assets.py b/pytorrent/services/frontend_assets.py index 93638ab..a1a75cc 100644 --- a/pytorrent/services/frontend_assets.py +++ b/pytorrent/services/frontend_assets.py @@ -188,3 +188,36 @@ def validate_offline_assets() -> None: "Run: ./scripts/download_frontend_libs.py or ./install.sh\n" f"Missing files:\n{preview}{extra}" ) + + +_STATIC_HASH_CACHE: dict[tuple[str, int], str] = {} + +def static_hash(static_root: Path | None = None) -> str: + """Return one short hash for all app static files. + + Note: This value is used as the shared browser-cache version, so any static file + change invalidates app.js imports, CSS and local frontend assets together. + """ + import hashlib + + root = static_root or (BASE_DIR / "pytorrent" / "static") + files = [path for path in root.rglob("*") if path.is_file() and "tracker_favicons" not in path.parts] + fingerprint = f"{root}:{sum(path.stat().st_mtime_ns for path in files)}:{sum(path.stat().st_size for path in files)}" + cached = _STATIC_HASH_CACHE.get((fingerprint, len(files))) + if cached: + return cached + digest = hashlib.sha256() + for path in sorted(files): + rel = path.relative_to(root).as_posix() + stat = path.stat() + digest.update(rel.encode("utf-8")) + digest.update(str(stat.st_size).encode("ascii")) + digest.update(str(stat.st_mtime_ns).encode("ascii")) + try: + digest.update(path.read_bytes()) + except OSError: + continue + value = digest.hexdigest()[:16] + _STATIC_HASH_CACHE.clear() + _STATIC_HASH_CACHE[(fingerprint, len(files))] = value + return value diff --git a/pytorrent/static/js/app.js b/pytorrent/static/js/app.js index 52e52e9..134eea8 100644 --- a/pytorrent/static/js/app.js +++ b/pytorrent/static/js/app.js @@ -1,186 +1,120 @@ -import { stateCoreSource } from './stateCore.js'; -import { columnStateSource } from './columnState.js'; -import { runtimeStateSource } from './runtimeState.js'; -import { sharedUiSource } from './sharedUi.js'; -import { torrentFilterHelpersSource } from './torrentFilterHelpers.js'; -import { torrentFilterUiSource } from './torrentFilterUi.js'; -import { torrentTrackerFiltersSource } from './torrentTrackerFilters.js'; -import { torrentTableStateSource } from './torrentTableState.js'; -import { torrentActionStateSource } from './torrentActionState.js'; -import { torrentRowRendererSource } from './torrentRowRenderer.js'; -import { torrentTableRendererSource } from './torrentTableRenderer.js'; -import { mobileSource } from './mobile.js'; -import { messagesSource } from './messages.js'; -import { torrentAddSource } from './torrentAdd.js'; -import { apiSource } from './api.js'; -import { createTorrentSource } from './createTorrent.js'; -import { torrentGeneralDetailsSource } from './torrentGeneralDetails.js'; -import { torrentFileDetailsSource } from './torrentFileDetails.js'; -import { torrentChunkDetailsSource } from './torrentChunkDetails.js'; -import { torrentPeerDetailsSource } from './torrentPeerDetails.js'; -import { torrentTrackerDetailsSource } from './torrentTrackerDetails.js'; -import { mobileTorrentDetailsSource } from './mobileTorrentDetails.js'; -import { torrentDetailsLoaderSource } from './torrentDetailsLoader.js'; -import { pathPickerToolsSource } from './pathPickerTools.js'; -import { columnManagerSource } from './columnManager.js'; -import { jobToolsSource } from './jobTools.js'; -import { labelToolsSource } from './labelTools.js'; -import { ratioToolsSource } from './ratioTools.js'; -import { rssToolsSource } from './rssTools.js'; -import { backupToolsSource } from './backupTools.js'; -import { smartQueueSource } from './smartQueue.js'; -import { rtorrentConfigSource } from './rtorrentConfig.js'; -import { appearancePreferencesSource } from './appearancePreferences.js'; -import { peerRefreshSource } from './peerRefresh.js'; -import { automationRulesSource } from './automationRules.js'; -import { cleanupToolsSource } from './cleanupTools.js'; -import { appDiagnosticsSource } from './appDiagnostics.js'; -import { footerPreferencesSource } from './footerPreferences.js'; -import { liveSpeedStatsSource } from './liveSpeedStats.js'; -import { statusBarSource } from './statusBar.js'; -import { preferencesToolsSource } from './preferencesTools.js'; -import { diskMonitorSource } from './diskMonitor.js'; -import { portCheckActionsSource } from './portCheckActions.js'; -import { appStatusSource } from './appStatus.js'; -import { torrentStatsSource } from './torrentStats.js'; -import { toolUiHelpersSource } from './toolUiHelpers.js'; -import { authUsersSource } from './authUsers.js'; -import { plannerToolsUiSource } from './plannerToolsUi.js'; -import { plannerSpeedControlsSource } from './plannerSpeedControls.js'; -import { plannerSettingsSource } from './plannerSettings.js'; -import { plannerPreviewHistorySource } from './plannerPreviewHistory.js'; -import { plannerActionsSource } from './plannerActions.js'; -import { smartViewsSource } from './smartViews.js'; -import { notificationCenterSource } from './notificationCenter.js'; -import { diagnosticsDashboardSource } from './diagnosticsDashboard.js'; -import { dashboardToolsSource } from './dashboardTools.js'; -import { operationLogsSource } from './operationLogs.js'; -import { pollerSettingsSource } from './pollerSettings.js'; -import { toolsModalSource } from './toolsModal.js'; -import { toolPaneEventsSource } from './toolPaneEvents.js'; -import { rssEventsSource } from './rssEvents.js'; -import { smartQueueEventsSource } from './smartQueueEvents.js'; -import { backupCleanupRtconfigEventsSource } from './backupCleanupRtconfigEvents.js'; -import { automationEventsSource } from './automationEvents.js'; -import { labelSmartEventsSource } from './labelSmartEvents.js'; -import { torrentSelectionEventsSource } from './torrentSelectionEvents.js'; -import { torrentTableEventsSource } from './torrentTableEvents.js'; -import { preferenceEventsSource } from './preferenceEvents.js'; -import { keyboardEventsSource } from './keyboardEvents.js'; -import { speedLimitControlsSource } from './speedLimitControls.js'; -import { themeMobileControlsSource } from './themeMobileControls.js'; -import { jobSettingsSource } from './jobSettings.js'; -import { profileListSource } from './profileList.js'; -import { profileFormSource } from './profileForm.js'; -import { profileActionsSource } from './profileActions.js'; -import { profileSelectionSource } from './profileSelection.js'; -import { realtimeChartsSource } from './realtimeCharts.js'; -import { trafficHistoryDataSource } from './trafficHistoryData.js'; -import { trafficChartRendererSource } from './trafficChartRenderer.js'; -import { initialSnapshotSource } from './initialSnapshot.js'; -import { footerStatusRefreshSource } from './footerStatusRefresh.js'; -import { systemStatsSocketSource } from './systemStatsSocket.js'; -import { mobileSelectEventsSource } from './mobileSelectEvents.js'; -import { bootstrapRuntimeSource } from './bootstrapRuntime.js'; - -export const moduleSources = [ - stateCoreSource, - columnStateSource, - runtimeStateSource, - sharedUiSource, - torrentFilterHelpersSource, - torrentFilterUiSource, - torrentTrackerFiltersSource, - torrentTableStateSource, - torrentActionStateSource, - torrentRowRendererSource, - torrentTableRendererSource, - mobileSource, - messagesSource, - torrentAddSource, - apiSource, - createTorrentSource, - torrentGeneralDetailsSource, - torrentFileDetailsSource, - torrentChunkDetailsSource, - torrentPeerDetailsSource, - torrentTrackerDetailsSource, - mobileTorrentDetailsSource, - torrentDetailsLoaderSource, - pathPickerToolsSource, - columnManagerSource, - jobToolsSource, - labelToolsSource, - ratioToolsSource, - rssToolsSource, - backupToolsSource, - smartQueueSource, - rtorrentConfigSource, - appearancePreferencesSource, - peerRefreshSource, - automationRulesSource, - cleanupToolsSource, - appDiagnosticsSource, - footerPreferencesSource, - liveSpeedStatsSource, - statusBarSource, - preferencesToolsSource, - diskMonitorSource, - portCheckActionsSource, - appStatusSource, - torrentStatsSource, - toolUiHelpersSource, - authUsersSource, - plannerToolsUiSource, - plannerSpeedControlsSource, - plannerSettingsSource, - plannerPreviewHistorySource, - plannerActionsSource, - smartViewsSource, - notificationCenterSource, - diagnosticsDashboardSource, - dashboardToolsSource, - operationLogsSource, - pollerSettingsSource, - toolsModalSource, - toolPaneEventsSource, - rssEventsSource, - smartQueueEventsSource, - backupCleanupRtconfigEventsSource, - automationEventsSource, - labelSmartEventsSource, - torrentSelectionEventsSource, - torrentTableEventsSource, - preferenceEventsSource, - keyboardEventsSource, - speedLimitControlsSource, - themeMobileControlsSource, - jobSettingsSource, - profileListSource, - profileFormSource, - profileActionsSource, - profileSelectionSource, - realtimeChartsSource, - trafficHistoryDataSource, - trafficChartRendererSource, - initialSnapshotSource, - footerStatusRefreshSource, - systemStatsSocketSource, - mobileSelectEventsSource, - bootstrapRuntimeSource, +const staticImportVersion = encodeURIComponent(String(window.PYTORRENT?.staticHash || 'dev')); +const versionedImport = (path) => import(`${path}?v=${staticImportVersion}`); +const moduleImportSpecs = [ + ['./stateCore.js', 'stateCoreSource'], + ['./columnState.js', 'columnStateSource'], + ['./runtimeState.js', 'runtimeStateSource'], + ['./sharedUi.js', 'sharedUiSource'], + ['./torrentFilterHelpers.js', 'torrentFilterHelpersSource'], + ['./torrentFilterUi.js', 'torrentFilterUiSource'], + ['./torrentTrackerFilters.js', 'torrentTrackerFiltersSource'], + ['./torrentTableState.js', 'torrentTableStateSource'], + ['./torrentActionState.js', 'torrentActionStateSource'], + ['./torrentRowRenderer.js', 'torrentRowRendererSource'], + ['./torrentTableRenderer.js', 'torrentTableRendererSource'], + ['./mobile.js', 'mobileSource'], + ['./messages.js', 'messagesSource'], + ['./torrentAdd.js', 'torrentAddSource'], + ['./api.js', 'apiSource'], + ['./createTorrent.js', 'createTorrentSource'], + ['./torrentGeneralDetails.js', 'torrentGeneralDetailsSource'], + ['./torrentFileDetails.js', 'torrentFileDetailsSource'], + ['./torrentChunkDetails.js', 'torrentChunkDetailsSource'], + ['./torrentPeerDetails.js', 'torrentPeerDetailsSource'], + ['./torrentTrackerDetails.js', 'torrentTrackerDetailsSource'], + ['./mobileTorrentDetails.js', 'mobileTorrentDetailsSource'], + ['./torrentDetailsLoader.js', 'torrentDetailsLoaderSource'], + ['./pathPickerTools.js', 'pathPickerToolsSource'], + ['./columnManager.js', 'columnManagerSource'], + ['./jobTools.js', 'jobToolsSource'], + ['./labelTools.js', 'labelToolsSource'], + ['./ratioTools.js', 'ratioToolsSource'], + ['./rssTools.js', 'rssToolsSource'], + ['./backupTools.js', 'backupToolsSource'], + ['./smartQueue.js', 'smartQueueSource'], + ['./rtorrentConfig.js', 'rtorrentConfigSource'], + ['./appearancePreferences.js', 'appearancePreferencesSource'], + ['./peerRefresh.js', 'peerRefreshSource'], + ['./automationRules.js', 'automationRulesSource'], + ['./cleanupTools.js', 'cleanupToolsSource'], + ['./appDiagnostics.js', 'appDiagnosticsSource'], + ['./footerPreferences.js', 'footerPreferencesSource'], + ['./liveSpeedStats.js', 'liveSpeedStatsSource'], + ['./statusBar.js', 'statusBarSource'], + ['./preferencesTools.js', 'preferencesToolsSource'], + ['./diskMonitor.js', 'diskMonitorSource'], + ['./portCheckActions.js', 'portCheckActionsSource'], + ['./appStatus.js', 'appStatusSource'], + ['./torrentStats.js', 'torrentStatsSource'], + ['./toolUiHelpers.js', 'toolUiHelpersSource'], + ['./authUsers.js', 'authUsersSource'], + ['./plannerToolsUi.js', 'plannerToolsUiSource'], + ['./plannerSpeedControls.js', 'plannerSpeedControlsSource'], + ['./plannerSettings.js', 'plannerSettingsSource'], + ['./plannerPreviewHistory.js', 'plannerPreviewHistorySource'], + ['./plannerActions.js', 'plannerActionsSource'], + ['./smartViews.js', 'smartViewsSource'], + ['./notificationCenter.js', 'notificationCenterSource'], + ['./diagnosticsDashboard.js', 'diagnosticsDashboardSource'], + ['./dashboardTools.js', 'dashboardToolsSource'], + ['./operationLogs.js', 'operationLogsSource'], + ['./pollerSettings.js', 'pollerSettingsSource'], + ['./toolsModal.js', 'toolsModalSource'], + ['./toolPaneEvents.js', 'toolPaneEventsSource'], + ['./rssEvents.js', 'rssEventsSource'], + ['./smartQueueEvents.js', 'smartQueueEventsSource'], + ['./backupCleanupRtconfigEvents.js', 'backupCleanupRtconfigEventsSource'], + ['./automationEvents.js', 'automationEventsSource'], + ['./labelSmartEvents.js', 'labelSmartEventsSource'], + ['./torrentSelectionEvents.js', 'torrentSelectionEventsSource'], + ['./torrentTableEvents.js', 'torrentTableEventsSource'], + ['./preferenceEvents.js', 'preferenceEventsSource'], + ['./keyboardEvents.js', 'keyboardEventsSource'], + ['./speedLimitControls.js', 'speedLimitControlsSource'], + ['./themeMobileControls.js', 'themeMobileControlsSource'], + ['./jobSettings.js', 'jobSettingsSource'], + ['./profileList.js', 'profileListSource'], + ['./profileForm.js', 'profileFormSource'], + ['./profileActions.js', 'profileActionsSource'], + ['./profileSelection.js', 'profileSelectionSource'], + ['./realtimeCharts.js', 'realtimeChartsSource'], + ['./trafficHistoryData.js', 'trafficHistoryDataSource'], + ['./trafficChartRenderer.js', 'trafficChartRendererSource'], + ['./initialSnapshot.js', 'initialSnapshotSource'], + ['./footerStatusRefresh.js', 'footerStatusRefreshSource'], + ['./systemStatsSocket.js', 'systemStatsSocketSource'], + ['./mobileSelectEvents.js', 'mobileSelectEventsSource'], + ['./bootstrapRuntime.js', 'bootstrapRuntimeSource'], ]; -export function buildRuntimeSource(){ - return `(() => {\n${moduleSources.join('\n')}\n})();\n`; +export let moduleSources = []; +let moduleSourcesPromise = null; + +async function loadModuleSources(){ + if(moduleSourcesPromise) return moduleSourcesPromise; + moduleSourcesPromise = Promise.all(moduleImportSpecs.map(([path]) => versionedImport(path))).then((modules) => { + moduleSources = modules.map((mod, index) => mod[moduleImportSpecs[index][1]]); + return moduleSources; + }); + return moduleSourcesPromise; } -export function startApp(){ - const runtimeSource = buildRuntimeSource(); +export async function buildRuntimeSource(){ + const sources = await loadModuleSources(); + return `(() => {\n${sources.join('\n')}\n})();\n`; +} + +export async function startApp(){ + const runtimeSource = await buildRuntimeSource(); // Keep the original shared lexical scope while loading the source from smaller ES modules. // `io` is passed explicitly so Socket.IO remains available inside the generated runtime. return Function('io', runtimeSource)(window.io); } if(typeof window !== 'undefined' && !window.PYTORRENT_DISABLE_AUTOSTART){ - startApp(); + startApp().catch((error) => { + console.error('pyTorrent frontend failed to start', error); + const loaderText = document.getElementById('initialLoaderText'); + if(loaderText) loaderText.textContent = 'Frontend failed to start. Reload the page or clear browser cache.'; + }); } diff --git a/pytorrent/static/js/bootstrapRuntime.js b/pytorrent/static/js/bootstrapRuntime.js index 57c142d..1932350 100644 --- a/pytorrent/static/js/bootstrapRuntime.js +++ b/pytorrent/static/js/bootstrapRuntime.js @@ -1 +1 @@ -export const bootstrapRuntimeSource = " updateSortHeaders(); setupColumnResizers(); applyColumnVisibility(); renderColumnManager(); restoreFooterStatusCache(); refreshFooterStatusNow(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); updateBrowserSpeedTitle(); setupTorrentDropZone(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); ensureDashboardToolsUI(); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); if(hasActiveProfile) refreshUserDiskUsage(true).catch(()=>{}); scheduleTrackerSummary(true);\n"; +export const bootstrapRuntimeSource = " async function checkStaticAssetVersion(){ try{ const r=await fetch('/api/static_hash',{cache:'no-store'}); const j=await r.json(); const current=String(window.PYTORRENT?.staticHash||''); const next=String(j.static_hash||j.version||''); if(current && next && current!==next){ window.PYTORRENT.staticHash=next; toast('A new frontend version is available. Reloading...','info'); setTimeout(()=>window.location.reload(), 600); } }catch(e){} }\n setInterval(checkStaticAssetVersion, 120000);\n window.addEventListener('focus', checkStaticAssetVersion);\n updateSortHeaders(); setupColumnResizers(); applyColumnVisibility(); renderColumnManager(); restoreFooterStatusCache(); refreshFooterStatusNow(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); updateBrowserSpeedTitle(); setupTorrentDropZone(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); ensureDashboardToolsUI(); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); if(hasActiveProfile) refreshUserDiskUsage(true).catch(()=>{}); scheduleTrackerSummary(true);\\n"; diff --git a/pytorrent/static/styles.css b/pytorrent/static/styles.css index 14a7ae4..7463925 100644 --- a/pytorrent/static/styles.css +++ b/pytorrent/static/styles.css @@ -2979,17 +2979,15 @@ body.mobile-mode .mobile-filter-bar { gap: 0.3rem; } -.smart-surge-refill-card .smart-refill-controls { - grid-template-columns: minmax(84px, 0.6fr) minmax(110px, 1fr) minmax(110px, 1fr); - width: min(450px, 100%); +.smart-surge-cooldown-card { + background: rgba(var(--bs-info-rgb), 0.08); } -.smart-refill-switch { - align-content: end; -} - -.smart-refill-switch .form-check-input { - margin: 0; +.smart-surge-refill-controls { + display: grid; + grid-template-columns: repeat(2, minmax(110px, 1fr)); + gap: 0.55rem; + width: min(330px, 100%); } .disk-monitor-shell { display: grid; @@ -3065,6 +3063,7 @@ body.mobile-mode .mobile-filter-bar { flex-direction: column; } + .smart-surge-refill-controls, .disk-monitor-shell { grid-template-columns: 1fr; } diff --git a/pytorrent/templates/index.html b/pytorrent/templates/index.html index 9e46009..e8cc39c 100644 --- a/pytorrent/templates/index.html +++ b/pytorrent/templates/index.html @@ -326,7 +326,7 @@
RSS downloader
Feeds are checked by schedule and every match is logged per feed/rule.
Feed
Rule
Feeds, rules and matches
Choose columns visible in the torrent list.
-
Smart Queue
Automatic queue balancing for slow or stalled downloads.
Run Smart Queue during polling. Stopped torrents are managed; Paused torrents stay user-controlled.
When enabled, Smart Queue disables itself after a check finds no active downloads and no waiting stopped candidates. Enable it again manually when you add more work.
Next Smart Queue runnext: readyAutomatic runs use the cooldown below. Manual Check now still runs immediately.
Queue refill during cooldownAutomatic keeps the current poller cadence. Custom runs only after the selected number of minutes. Off disables refill completely.
Surge refillPeriodically starts a large batch above the active-download target. Normal Smart Queue checks keep replacing stalled items and drain overflow back toward the target.
Recommended for best efficiency. When enabled, Smart Queue refills empty slots first and does not stop stalled downloads while active downloads are below the cap. Stalled cleanup resumes once the cap is reached or exceeded. Disable only if you prefer aggressive cleanup over keeping the active count near the cap.
Start stopped torrents with existing progress first, so Smart Queue finishes already started downloads before opening fresh ones.
When enabled, Smart Queue does not use seed/peer count as a stalled criterion.
When enabled, low speed is not required. With source and speed ignores enabled, only Stalled after seconds decides.
Choose torrents ignored by Smart Queue. Existing behavior stays unchanged for all non-excluded torrents.
Last operations
+
Smart Queue
Automatic queue balancing for slow or stalled downloads.
Run Smart Queue during polling. Stopped torrents are managed; Paused torrents stay user-controlled.
When enabled, Smart Queue disables itself after a check finds no active downloads and no waiting stopped candidates. Enable it again manually when you add more work.
Next Smart Queue runnext: readyAutomatic runs use the cooldown below. Manual Check now still runs immediately.
Queue refill during cooldownAutomatic keeps the current poller cadence. Custom runs only after the selected number of minutes. Off disables refill completely.
Periodically start a large batch above the active-download target. Normal Smart Queue checks keep replacing stalled items and drain overflow back toward the target.
Next Surge refill runnext: readyAutomatic Surge refill uses the interval below. Manual Smart Queue check still ignores this timer.
Recommended for best efficiency. When enabled, Smart Queue refills empty slots first and does not stop stalled downloads while active downloads are below the cap. Stalled cleanup resumes once the cap is reached or exceeded. Disable only if you prefer aggressive cleanup over keeping the active count near the cap.
Start stopped torrents with existing progress first, so Smart Queue finishes already started downloads before opening fresh ones.
When enabled, Smart Queue does not use seed/peer count as a stalled criterion.
When enabled, low speed is not required. With source and speed ignores enabled, only Stalled after seconds decides.
Choose torrents ignored by Smart Queue. Existing behavior stays unchanged for all non-excluded torrents.
Last operations
Automations / rules
Build a rule as: conditions first, then ordered actions. Matching torrents are handled as one batch and the cooldown is applied to the whole rule.
1. Rule
2. Conditions
3. Actions, in order
Rules
History
rTorrent config
Grouped rTorrent runtime settings with inline recommendations and compatibility status.
Reference value is kept from the first override save. Later saves add or clear differences without replacing the original reference.
No changes
Loading config...
Cleanup / retention
One place to clear logs and active profile caches. Pending/running jobs, rules, settings and torrents are preserved.
Loading cleanup data...
@@ -407,7 +407,7 @@
- +