Files
pyTorrent/pytorrent/templates/index.html
Mateusz Gruszczyński c19ff17134 offline libs
2026-05-06 11:25:41 +02:00

189 lines
46 KiB
HTML

<!doctype html>
<html lang="en" data-bs-theme="{{ prefs.theme if prefs else 'dark' }}" data-app-font="{{ prefs.font_family if prefs and prefs.font_family else 'default' }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>pyTorrent</title>
<link id="bootstrapThemeStylesheet" href="{{ bootstrap_theme_url(prefs.bootstrap_theme if prefs else 'default') }}" rel="stylesheet">
<link href="{{ frontend_asset_url('fontawesome_css') }}" rel="stylesheet">
<link href="{{ frontend_asset_url('flag_icons_css') }}" rel="stylesheet">
<link href="{{ static_url('styles.css') }}" rel="stylesheet">
</head>
<body>
<div id="initialLoader" class="initial-loader" role="status" aria-live="polite">
<div class="initial-loader-card">
<div class="initial-loader-brand"><i class="fa-solid fa-robot"></i> pyTorrent</div>
<div class="initial-loader-spinner"><span class="spinner-border" aria-hidden="true"></span></div>
<div id="initialLoaderTitle" class="initial-loader-title">Loading torrents...</div>
<div id="initialLoaderText" class="initial-loader-text">Connecting to rTorrent and preparing the first data snapshot.</div>
</div>
</div>
<div id="globalLoader" class="global-loader d-none"><span class="spinner-border spinner-border-sm"></span><span>Working...</span></div>
<div class="app-shell">
<header class="topbar border-bottom">
<div class="toolbar-left">
<div class="brand"><i class="fa-solid fa-robot"></i> pyTorrent</div>
<button id="profilePickerBtn" class="btn btn-xs btn-outline-primary nav-btn profile-picker-btn" data-bs-toggle="modal" data-bs-target="#profilePickerModal" title="Change rTorrent">
<i class="fa-solid fa-server"></i><span id="activeProfileName">{{ active_profile.name if active_profile else 'Add rTorrent' }}</span>
</button>
<button class="btn btn-xs btn-outline-primary nav-btn" data-bs-toggle="modal" data-bs-target="#trafficModal"><i class="fa-solid fa-chart-column"></i><span> History</span></button>
<button class="btn btn-xs btn-success nav-btn" data-bs-toggle="modal" data-bs-target="#addModal"><i class="fa-solid fa-plus"></i><span> Add</span></button>
<button class="btn btn-xs btn-outline-secondary nav-btn" data-bs-toggle="modal" data-bs-target="#jobsModal"><i class="fa-solid fa-list-check"></i><span> Jobs</span></button>
<button class="btn btn-xs btn-outline-secondary nav-btn" data-bs-toggle="modal" data-bs-target="#toolsModal"><i class="fa-solid fa-sliders"></i><span> Tools</span></button>
<button class="btn btn-xs btn-outline-success torrent-action nav-btn" data-action="start" title="Start"><i class="fa-solid fa-play"></i></button>
<button class="btn btn-xs btn-outline-warning torrent-action nav-btn" data-action="pause" title="Pause"><i class="fa-solid fa-pause"></i></button>
<button class="btn btn-xs btn-outline-secondary torrent-action nav-btn" data-action="stop" title="Stop"><i class="fa-solid fa-stop"></i></button>
<button class="btn btn-xs btn-outline-info torrent-action nav-btn" data-action="recheck" title="Force recheck"><i class="fa-solid fa-rotate"></i></button><button class="btn btn-xs btn-outline-primary torrent-action nav-btn" data-action="reannounce" title="Reannounce"><i class="fa-solid fa-bullhorn"></i></button>
<button class="btn btn-xs btn-outline-danger nav-btn" data-bs-toggle="modal" data-bs-target="#removeModal" title="Remove"><i class="fa-solid fa-trash"></i></button>
</div>
<div class="toolbar-right">
<input id="searchBox" class="form-control form-control-sm search" placeholder="Search torrents..."><span id="mobileSpeedStats" class="mobile-speed-stats" aria-label="Current transfer speed"><span><i class="fa-solid fa-down-long"></i> <b id="mobileSpeedDl">0 B/s</b></span><span><i class="fa-solid fa-up-long"></i> <b id="mobileSpeedUl">0 B/s</b></span></span>
<span id="busyBadge" class="badge text-bg-dark d-none"><span class="spinner-border spinner-border-xs"></span> busy</span>
<span id="connBadge" class="badge text-bg-secondary">offline</span>
<button class="btn btn-xs btn-outline-info nav-btn" id="mobileToggle" title="Mobile/simple mode"><i class="fa-solid fa-mobile-screen"></i></button>
<button id="themeToggle" class="btn btn-xs btn-outline-secondary nav-btn" title="Change theme"><i class="fa-solid fa-moon"></i></button>
<button class="btn btn-xs btn-outline-secondary nav-btn about-nav-btn" data-bs-toggle="modal" data-bs-target="#aboutModal" title="About pyTorrent"><i class="fa-solid fa-circle-info"></i></button>
{% if auth_enabled %}<a class="btn btn-xs btn-outline-danger nav-btn" href="/logout" title="Log out"><i class="fa-solid fa-right-from-bracket"></i><span> {{ current_user.username if current_user else 'logout' }}</span></a>{% endif %}
</div>
</header>
<main class="main-grid">
<aside class="sidebar border-end">
<button class="filter active" data-filter="all"><span><i class="fa-solid fa-list me-1"></i>All</span> <span id="countAll">0</span></button>
<button class="filter" data-filter="downloading"><span><i class="fa-solid fa-download me-1"></i>Downloading</span> <span id="countDownloading">0</span></button>
<button class="filter" data-filter="seeding"><span><i class="fa-solid fa-seedling me-1"></i>Seeding</span> <span id="countSeeding">0</span></button>
<button class="filter" data-filter="paused"><span><i class="fa-solid fa-pause me-1"></i>Paused</span> <span id="countPaused">0</span></button>
<button class="filter" data-filter="checking"><span><i class="fa-solid fa-rotate me-1"></i>Checking</span> <span id="countChecking">0</span></button>
<button class="filter" data-filter="error"><span><i class="fa-solid fa-triangle-exclamation me-1"></i>With error</span> <span id="countError">0</span></button>
<button class="filter" data-filter="stopped"><span><i class="fa-solid fa-stop me-1"></i>Stopped</span> <span id="countStopped">0</span></button>
<button class="filter d-none" data-filter="moving"><span><i class="fa-solid fa-folder-open me-1"></i>Moving</span> <span id="countMoving">0</span></button>
<div id="labelFilters" class="label-filters mt-2"></div>
<hr>
<div class="small text-muted px-2">Shortcuts</div>
<div class="shortcut">Ctrl+A — select visible</div>
<div class="shortcut">Ctrl+I — invert visible</div>
<div class="shortcut">Space — start</div>
<div class="shortcut">P — pause</div>
<div class="shortcut">S — stop</div>
<div class="shortcut">R — resume</div>
<div class="shortcut">M — move</div>
<div class="shortcut">Esc — clear selection</div>
<div class="shortcut">Delete — remove</div>
<div class="shortcut">Ctrl+O — add</div>
</aside>
<section class="content">
<div id="bulkBar" class="bulk-bar d-none"><span><b id="bulkSelectedCount">0</b> selected</span><button class="btn btn-xs btn-outline-primary" data-bs-toggle="modal" data-bs-target="#labelModal"><i class="fa-solid fa-tag"></i> Label</button><button class="btn btn-xs btn-outline-primary" data-bs-toggle="modal" data-bs-target="#ratioAssignModal"><i class="fa-solid fa-scale-balanced"></i> Ratio</button><button class="btn btn-xs btn-outline-primary" data-action="move"><i class="fa-solid fa-folder-open"></i> Move</button><button id="bulkClearBtn" class="btn btn-xs btn-outline-secondary ms-auto"><i class="fa-solid fa-xmark"></i> Clear</button></div><div id="tableWrap" class="table-wrap">
<table class="table table-hover table-sm align-middle torrent-table">
<thead><tr>
<th class="sel" data-col="select"><input id="selectAll" type="checkbox"></th>
<th data-sort="name" data-col="name">Name</th><th data-sort="status" data-col="status">Status</th><th data-sort="size" data-col="size">Size</th><th data-sort="progress" data-col="progress">Progress</th>
<th data-sort="down_rate" data-col="down_rate">DL</th><th data-sort="up_rate" data-col="up_rate">UL</th><th data-sort="seeds" data-col="seeds">Seeds</th><th data-sort="peers" data-col="peers">Peers</th>
<th data-sort="ratio" data-col="ratio">Ratio</th><th data-sort="path" data-col="path">Path</th><th data-sort="label" data-col="label">Label</th><th data-sort="ratio_group" data-col="ratio_group">Ratio group</th>
</tr></thead>
<tbody id="torrentBody"><tr><td colspan="13" class="empty"><span class="spinner-border spinner-border-sm me-2"></span>Waiting for data.</td></tr></tbody>
</table>
</div>
<div id="mobileFilterBar" class="mobile-filter-bar d-none" aria-label="Torrent filters"></div>
<div id="mobileList" class="mobile-list d-none"></div>
<div class="details border-top">
<ul class="nav nav-tabs" id="detailTabs">
<li class="nav-item"><button class="nav-link active" data-tab="general"><i class="fa-solid fa-circle-info"></i> General</button></li>
<li class="nav-item"><button class="nav-link" data-tab="files"><i class="fa-solid fa-file-lines"></i> Files</button></li>
<li class="nav-item"><button class="nav-link" data-tab="peers"><i class="fa-solid fa-users"></i> Peers</button></li>
<li class="nav-item"><button class="nav-link" data-tab="trackers"><i class="fa-solid fa-bullseye"></i> Trackers</button></li>
<li class="nav-item"><button class="nav-link" data-tab="log"><i class="fa-solid fa-terminal"></i> Log</button></li>
</ul>
<div id="peersRefreshBox" class="peers-refresh d-none"><label class="form-label mb-0 small">Peers auto refresh</label><select id="peersRefreshSelect" class="form-select form-select-sm"><option value="0">Off</option><option value="10">10s</option><option value="15">15s</option><option value="30">30s</option><option value="60">60s</option></select></div>
<div id="detailPane" class="detail-pane muted-pane">Select a torrent.</div>
</div>
</section>
</main>
<footer class="statusbar border-top">
<span id="statCpuBox" data-footer-item="cpu">CPU <b id="statCpu">-</b>%</span><span id="statRamBox" data-footer-item="ram">RAM <b id="statRam">-</b>%</span><canvas id="systemChart" class="system-chart" data-footer-item="usage_chart" width="96" height="24" title="CPU / RAM usage"></canvas>
<span id="diskStatus" class="disk-status" data-footer-item="disk" title="Disk usage unavailable"><i class="fa-solid fa-hard-drive"></i><canvas id="diskChart" width="46" height="24"></canvas><b id="statDisk">-</b></span>
<span data-footer-item="version">rTorrent <b id="statVersion">-</b></span><span data-footer-item="speed_down">DL <b id="statDl">0 B/s</b></span><span data-footer-item="speed_up">UL <b id="statUl">0 B/s</b></span>
<button class="status-limit" data-footer-item="limits" data-bs-toggle="modal" data-bs-target="#speedModal">Limit DL <b id="statDlLimit"></b> / UL <b id="statUlLimit"></b> <i class="fa-solid fa-pen-to-square"></i></button>
<span data-footer-item="totals">Total DL/UP <b id="statTotalDl">0B</b>/<b id="statTotalUl">0B</b></span><span id="statusPortCheck" class="d-none" data-footer-item="port_check" title="Port check disabled"><span id="statusPortCheckBadge" class="port-status port-secondary"><i class="fa-solid fa-circle-question"></i> Port - unknown</span></span><span id="statusClock" data-footer-item="clock" title="Local browser time"><i class="fa-solid fa-clock"></i> <b id="statClock">--:--:--</b></span><span id="statusSockets" data-footer-item="sockets" title="Open rTorrent sockets"><i class="fa-solid fa-network-wired"></i> Sockets <b id="statSockets">-</b></span><span data-footer-item="shown">Shown <b id="statShown">0</b></span><span data-footer-item="selected">Selected <b id="statSelected">0</b></span><a class="status-docs" data-footer-item="docs" href="/docs" target="_blank" rel="noopener"><i class="fa-solid fa-book"></i> Docs API</a>
</footer>
</div>
<div id="ctxMenu" class="ctx-menu shadow">
<button data-action="start"><i class="fa-solid fa-play"></i> Start</button><button data-action="pause"><i class="fa-solid fa-pause"></i> Pause</button><button data-action="stop"><i class="fa-solid fa-stop"></i> Stop</button><button data-action="recheck"><i class="fa-solid fa-rotate"></i> Force recheck</button><button data-action="move"><i class="fa-solid fa-folder-open"></i> Move...</button><hr>
<button data-bs-toggle="modal" data-bs-target="#labelModal"><i class="fa-solid fa-tag"></i> Set label...</button><button data-bs-toggle="modal" data-bs-target="#ratioAssignModal"><i class="fa-solid fa-scale-balanced"></i> Set ratio group...</button><button id="smartExcludeCtx"><i class="fa-solid fa-ban"></i> Exclude from Smart Queue</button><hr>
<button data-copy="hash"><i class="fa-solid fa-hashtag"></i> Copy hash</button><button data-copy="name"><i class="fa-solid fa-font"></i> Copy name</button><button data-copy="path"><i class="fa-solid fa-folder-tree"></i> Copy path</button><hr><button data-bs-toggle="modal" data-bs-target="#removeModal" class="danger"><i class="fa-solid fa-trash"></i> Remove...</button>
</div>
<div class="modal fade" id="profilePickerModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fa-solid fa-server"></i> Choose rTorrent</h5>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<label class="form-label" for="profileSelect">Configured rTorrents</label>
<select id="profileSelect" class="form-select profile-select">
{% for p in profiles %}<option value="{{ p.id }}" {% if active_profile and active_profile.id == p.id %}selected{% endif %}>{{ p.name }}</option>{% endfor %}
</select>
<div class="form-text">Changing rTorrent reloads the live torrent snapshot.</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="removeModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Remove selected torrents</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p><b id="removeCount">0</b> selected torrents will be removed.</p><div class="form-check form-switch"><input id="removeData" class="form-check-input" type="checkbox" checked><label class="form-check-label" for="removeData">Remove with data</label></div></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal"><i class="fa-solid fa-xmark"></i> Cancel</button><button id="confirmRemoveBtn" class="btn btn-danger"><i class="fa-solid fa-trash"></i> Remove</button></div></div></div></div>
<div class="modal fade" id="addModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Add torrent / magnet</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body add-grid"><div><label class="form-label">Magnet links</label><textarea id="magnetInput" class="form-control magnet-box" rows="2" placeholder="One magnet per line"></textarea></div><div class="upload-box"><label class="form-label">Torrent file or multiple .torrent files</label><input id="torrentFiles" class="form-control" type="file" accept=".torrent,application/x-bittorrent" multiple><div id="torrentFilesInfo" class="form-text">You can select multiple files at once.</div></div><div class="row g-2 align-items-end"><div class="col-md-7"><label class="form-label">Save location</label><div class="input-group"><input id="addPath" class="form-control" placeholder="/data/torrents"><button class="btn btn-outline-secondary browse-path" data-target="addPath" type="button"><i class="fa-solid fa-folder-open"></i></button></div></div><div class="col-md-3"><label class="form-label">Label</label><input id="addLabel" class="form-control" placeholder="movies"></div><div class="col-md-2"><div class="form-check form-switch mb-2"><input id="addStart" class="form-check-input" type="checkbox" checked><label class="form-check-label" for="addStart">Start</label></div></div></div></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal"><i class="fa-solid fa-xmark"></i> Close</button><button id="addBtn" class="btn btn-success"><span class="btn-label"><i class="fa-solid fa-plus"></i> Add</span></button></div></div></div></div>
<div class="modal fade" id="speedModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Speed limits</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="row g-2"><div class="col"><label class="form-label">Download KiB/s</label><input id="limitDown" class="form-control" type="number" min="0"></div><div class="col"><label class="form-label">Upload KiB/s</label><input id="limitUp" class="form-control" type="number" min="0"></div></div><div class="limit-slider-panel mt-3"><div class="limit-slider-row"><label for="limitDownSlider" class="form-label">Custom download <b id="limitDownMbps">0 Mbit/s</b></label><input id="limitDownSlider" class="form-range limit-slider" type="range" min="0" max="2000" step="1" value="0" data-target="limitDown" data-output="limitDownMbps"></div><div class="limit-slider-row"><label for="limitUpSlider" class="form-label">Custom upload <b id="limitUpMbps">0 Mbit/s</b></label><input id="limitUpSlider" class="form-range limit-slider" type="range" min="0" max="2000" step="1" value="0" data-target="limitUp" data-output="limitUpMbps"></div><div class="small text-muted">0 means unlimited. Sliders use Mbit/s and save through the existing speed limits API.</div></div><div class="preset-grid mt-3"><button class="btn btn-sm btn-outline-secondary limit-preset" data-mbps="0"><i class="fa-solid fa-infinity"></i> Unlimited</button><button class="btn btn-sm btn-outline-secondary limit-preset" data-mbps="100"><i class="fa-solid fa-gauge"></i> 100 Mbit/s</button><button class="btn btn-sm btn-outline-secondary limit-preset" data-mbps="200"><i class="fa-solid fa-gauge"></i> 200 Mbit/s</button><button class="btn btn-sm btn-outline-secondary limit-preset" data-mbps="500"><i class="fa-solid fa-gauge-high"></i> 500 Mbit/s</button><button class="btn btn-sm btn-outline-secondary limit-preset" data-mbps="1000"><i class="fa-solid fa-gauge-high"></i> 1 Gbit/s</button><button class="btn btn-sm btn-outline-secondary limit-preset" data-mbps="2000"><i class="fa-solid fa-gauge-high"></i> 2 Gbit/s</button></div></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal"><i class="fa-solid fa-xmark"></i> Close</button><button id="saveSpeedBtn" class="btn btn-primary"><span class="btn-label"><i class="fa-solid fa-floppy-disk"></i> Save limits</span></button></div></div></div></div>
<div class="modal fade" id="jobsModal" tabindex="-1"><div class="modal-dialog modal-xl"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="fa-solid fa-list-check"></i> Job queue</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="d-flex gap-2 mb-2"><button id="refreshJobsBtn" class="btn btn-sm btn-outline-primary"><i class="fa-solid fa-rotate"></i> Refresh</button><button id="clearJobsBtn" class="btn btn-sm btn-outline-danger"><i class="fa-solid fa-trash"></i> Clear finished</button><span class="text-muted small align-self-center">Pending, running, done, failed, retry and cancel history.</span></div><div id="jobsTable" class="table-responsive"><span class="spinner-border spinner-border-sm"></span> Loading jobs...</div><div id="jobsPager" class="pager-row mt-2"></div></div></div></div></div>
<div class="modal fade" id="pathModal" tabindex="-1"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Choose path</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="input-group mb-2"><input id="pathCurrent" class="form-control"><button id="pathGoBtn" class="btn btn-outline-secondary"><i class="fa-solid fa-folder-open"></i> Open</button><button id="pathUpBtn" class="btn btn-outline-secondary"><i class="fa-solid fa-arrow-up"></i> Up</button><button id="pathReloadBtn" class="btn btn-outline-secondary" title="Reload"><i class="fa-solid fa-rotate"></i></button></div><div id="pathList" class="path-list"><span class="spinner-border spinner-border-sm"></span> Loading...</div><div id="moveOptions" class="move-options d-none mt-3"><div class="form-check form-switch"><input id="moveDataPhysical" class="form-check-input" type="checkbox"><label class="form-check-label" for="moveDataPhysical">Move data physically on disk</label></div><div class="form-check form-switch"><input id="moveRecheck" class="form-check-input" type="checkbox" checked><label class="form-check-label" for="moveRecheck">Force recheck after physical move</label></div><div class="form-text">Unchecked: only rTorrent path is changed. Checked: torrent is stopped/closed, files are moved one by one by the job queue, path is updated, then torrent is started again if it was active.</div></div></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal"><i class="fa-solid fa-xmark"></i> Cancel</button><button id="pathSelectBtn" class="btn btn-primary"><i class="fa-solid fa-check"></i> Use this path</button></div></div></div></div>
<div class="modal fade" id="labelModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="fa-solid fa-tags"></i> Set labels</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><label class="form-label small text-muted">Add new label</label><div class="input-group mb-3"><input id="labelInput" class="form-control" placeholder="Label name, or several separated by comma"><button id="addLabelToSelectionBtn" class="btn btn-outline-primary" type="button"><i class="fa-solid fa-plus"></i> Add</button></div><div class="section-title">Selected labels</div><div id="selectedLabelList" class="chips mb-3"></div><div class="section-title">Saved labels</div><div id="labelList" class="chips mt-2"></div></div><div class="modal-footer"><button id="clearLabelsBtn" class="btn btn-outline-danger me-auto"><i class="fa-solid fa-eraser"></i> Clear labels</button><button class="btn btn-secondary" data-bs-dismiss="modal"><i class="fa-solid fa-xmark"></i> Close</button><button id="saveLabelBtn" class="btn btn-primary"><i class="fa-solid fa-check"></i> Apply</button></div></div></div></div>
<div class="modal fade" id="ratioAssignModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Set ratio group</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><select id="ratioAssignSelect" class="form-select"></select></div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal"><i class="fa-solid fa-xmark"></i> Close</button><button id="applyRatioBtn" class="btn btn-primary"><i class="fa-solid fa-check"></i> Apply</button></div></div></div></div>
<div class="modal fade" id="toolsModal" tabindex="-1"><div class="modal-dialog modal-xl"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Tools & rTorrents</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><ul class="nav nav-pills mb-3"><li class="nav-item"><button class="nav-link active tool-tab" data-tool="rtorrents"><i class="fa-solid fa-server"></i> rTorrents</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="torrentstats"><i class="fa-solid fa-chart-pie"></i> Torrent stats</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="preferences"><i class="fa-solid fa-sliders"></i> Preferences</button></li>{% if auth_enabled and current_user and current_user.role == 'admin' %}<li class="nav-item"><button class="nav-link tool-tab" data-tool="users"><i class="fa-solid fa-users-gear"></i> Users</button></li>{% endif %}<li class="nav-item"><button class="nav-link tool-tab" data-tool="labels"><i class="fa-solid fa-tags"></i> Labels</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="ratio"><i class="fa-solid fa-scale-balanced"></i> Ratio groups</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="rss"><i class="fa-solid fa-rss"></i> RSS downloader</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="columns"><i class="fa-solid fa-table-columns"></i> Columns</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="smart"><i class="fa-solid fa-shuffle"></i> Smart Queue</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="automations"><i class="fa-solid fa-wand-magic-sparkles"></i> Automations</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="rtconfig"><i class="fa-solid fa-gears"></i> rTorrent config</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="cleanup"><i class="fa-solid fa-broom"></i> Cleanup / retention</button></li><li class="nav-item"><button class="nav-link tool-tab" data-tool="appstatus"><i class="fa-solid fa-heart-pulse"></i> App status</button></li></ul><div id="toolRtorrents"><div class="surface-section mb-3"><div class="section-title">rTorrents</div><div id="profileList" class="mb-3 small"><span class="spinner-border spinner-border-sm me-2"></span>Loading profiles...</div></div><div class="surface-section mb-3"><div class="section-title"><span id="profileFormTitle">Add rTorrent profile</span></div><input id="profileId" type="hidden"><div class="profile-form-grid"><label class="profile-form-field"><span>Profile name</span><input id="profileName" class="form-control" placeholder="rtorrent1"><small>Visible name used in the profile selector.</small></label><label class="profile-form-field profile-form-field-wide"><span>SCGI URL</span><input id="profileUrl" class="form-control" placeholder="scgi://127.0.0.1:5000/RPC2"><small>Connection address in scgi://host:port/RPC2 format.</small></label><label class="profile-form-field"><span>Timeout</span><input id="profileTimeout" class="form-control" type="number" value="5" min="1"><small>Seconds to wait for rTorrent response.</small></label><label class="profile-form-field"><span>Parallel jobs</span><input id="profileParallel" class="form-control" type="number" value="5" min="1"><small>Maximum queued actions running at once.</small></label><label class="profile-form-field profile-check-field"><span>Location</span><span class="form-check form-switch mb-0"><input id="profileRemote" class="form-check-input" type="checkbox"><span class="form-check-label">Remote location</span></span><small>Check this if you want to connect to a remote rTorrent instance instead of localhost.</small></label><div class="profile-form-actions"><button id="saveProfileBtn" class="btn btn-primary btn-sm"><i class="fa-solid fa-plus"></i> Add profile</button><button id="cancelProfileEditBtn" class="btn btn-outline-secondary btn-sm d-none" type="button"><i class="fa-solid fa-xmark"></i> Cancel</button></div></div><div class="form-text">Create one rTorrent profile at a time. Move/remove queues keep their order for each profile.</div></div></div><div id="toolTorrentStats" class="d-none"><div class="surface-section"><div class="section-title"><i class="fa-solid fa-chart-pie"></i> Torrent statistics</div><div class="tool-note mb-3">Cached metadata summary. File metadata is refreshed every 15 minutes, a few minutes after startup, or manually.</div><div class="torrent-stats-toolbar"><button id="torrentStatsRefreshBtn" class="btn btn-sm btn-outline-primary"><i class="fa-solid fa-rotate"></i> Refresh now</button><span id="torrentStatsMeta" class="small text-muted">Not loaded.</span></div><div id="torrentStatsManager" class="mt-3">Open this tab to load statistics.</div></div></div><div id="toolPreferences" class="d-none"><div class="preferences-sections"><div class="surface-section preference-section"><div class="section-title"><i class="fa-solid fa-palette"></i> Appearance</div><div class="small text-muted mb-2">Theme and typography.</div><div class="preferences-grid"><label class="form-field"><span>Bootstrap theme</span><select id="bootstrapThemeSelect" class="form-select form-select-sm">{% for value, label in bootstrap_themes.items() %}<option value="{{ value }}" {% if prefs and prefs.bootstrap_theme == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><label class="form-field"><span>Font</span><select id="fontFamilySelect" class="form-select form-select-sm">{% for value, label in font_families.items() %}<option value="{{ value }}" {% if prefs and prefs.font_family == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label></div></div><div class="surface-section preference-section"><div class="section-title"><i class="fa-solid fa-network-wired"></i> Port checker</div><div class="small text-muted mb-2">Incoming connection test, separate from visual preferences.</div><div class="form-check form-switch mb-3"><input id="portCheckEnabled" class="form-check-input" type="checkbox"><label class="form-check-label" for="portCheckEnabled">Enable incoming port check</label></div><div class="d-flex gap-2 align-items-center flex-wrap"><span id="portCheckBadge" class="port-status port-secondary">disabled</span><button id="portCheckNowBtn" class="btn btn-sm btn-outline-primary"><i class="fa-solid fa-plug-circle-check"></i> Check port now</button></div><div id="portCheckInfo" class="form-text mt-2">Uses YouGetSignal first. Manual check bypasses the 6h cache.</div></div><div class="surface-section preference-section"><div class="section-title"><i class="fa-solid fa-table-list"></i> Footer</div><div class="small text-muted mb-2">Choose which status items are visible in the bottom bar.</div><div id="footerPreferences" class="footer-preferences"></div><button id="saveFooterPrefsBtn" class="btn btn-sm btn-primary mt-2" type="button"><i class="fa-solid fa-floppy-disk"></i> Save footer</button></div></div></div>{% if auth_enabled and current_user and current_user.role == 'admin' %}<div id="toolUsers" class="d-none"><div class="surface-section"><div class="section-title"><i class="fa-solid fa-users-gear"></i> Users</div><div class="tool-note mb-3">Manage optional pyTorrent users. Empty profile means all profiles. R/O blocks rTorrent-changing actions; Full allows them.</div><div class="user-form-grid"><input id="authUserId" type="hidden"><input id="authUsername" class="form-control" placeholder="User"><input id="authPassword" class="form-control" type="password" placeholder="Password / new password"><select id="authRole" class="form-select"><option value="user">user</option><option value="admin">admin</option></select><select id="authProfile" class="form-select"><option value="0">All profiles</option></select><select id="authAccess" class="form-select"><option value="ro">R/O</option><option value="full">Full</option></select><label class="form-check form-switch mb-0"><input id="authActive" class="form-check-input" type="checkbox" checked><span class="form-check-label">Active</span></label><button id="authUserSaveBtn" class="btn btn-sm btn-primary" type="button"><i class="fa-solid fa-floppy-disk"></i> Save user</button><button id="authUserCancelBtn" class="btn btn-sm btn-outline-secondary d-none" type="button"><i class="fa-solid fa-xmark"></i> Cancel</button></div><div id="authUsersManager" class="mt-3"></div></div></div>{% endif %}<div id="toolRtconfig" class="d-none"><div class="surface-section"><div class="section-title">rTorrent config</div><div class="small text-muted mb-2">Typical rTorrent options, like in ruTorrent. Unsupported methods are shown as unavailable.</div><div class="alert alert-secondary py-2 small rt-config-note">Reference value is kept from the first override save. Later saves add or clear differences without replacing the original reference.</div><div class="rt-config-toolbar"><span id="rtConfigChangedCount" class="badge text-bg-secondary">No changes</span><label class="form-check form-switch mb-0"><input id="rtConfigApplyOnStart" class="form-check-input" type="checkbox"><span class="form-check-label">Apply saved changes 60s after pyTorrent start</span></label></div><div id="rtConfigManager"><span class="spinner-border spinner-border-sm"></span> Loading config...</div><div class="mt-3"><button id="rtConfigReloadBtn" class="btn btn-sm btn-outline-primary"><i class="fa-solid fa-rotate"></i> Reload</button><button id="rtConfigGenerateBtn" class="btn btn-sm btn-outline-secondary ms-2" disabled><i class="fa-solid fa-file-code"></i> Generate config</button><button id="rtConfigSaveBtn" class="btn btn-sm btn-primary ms-2"><i class="fa-solid fa-floppy-disk"></i> Save config</button></div><textarea id="rtConfigOutput" class="form-control form-control-sm mt-3 rt-config-output" rows="7" readonly placeholder="Generated rTorrent config changes will appear here."></textarea></div></div><div id="toolCleanup" class="d-none"><div class="surface-section"><div class="section-title"><i class="fa-solid fa-broom"></i> Cleanup / retention</div><div class="tool-note mb-3">One place to review and clear job logs and Smart Queue history. Pending and running jobs are preserved.</div><div id="cleanupManager"><span class="spinner-border spinner-border-sm"></span> Loading cleanup data...</div></div></div><div id="toolAppstatus" class="d-none"><div class="surface-section"><div class="section-title"><i class="fa-solid fa-heart-pulse"></i> pyTorrent status</div><div class="small text-muted mb-2">Diagnostics for pyTorrent process and active SCGI/XMLRPC connection.</div><div class="mb-2"><button id="appStatusRefreshBtn" class="btn btn-sm btn-outline-primary"><i class="fa-solid fa-rotate"></i> Refresh</button></div><div id="appStatusManager">Open this tab to load diagnostics.</div></div></div><div id="toolLabels" class="d-none"><div class="row g-2 mb-2"><div class="col"><input id="newLabelName" class="form-control" placeholder="New label"></div><div class="col-auto"><button id="newLabelBtn" class="btn btn-primary"><i class="fa-solid fa-plus"></i> Add label</button></div></div><div id="labelsManager"></div></div><div id="toolRatio" class="d-none"><div class="row g-2 mb-2"><div class="col"><input id="ratioName" class="form-control" placeholder="Group name"></div><div class="col"><input id="ratioMin" class="form-control" type="number" step="0.1" value="1" placeholder="Min ratio"></div><div class="col"><input id="ratioMax" class="form-control" type="number" step="0.1" value="2" placeholder="Max ratio"></div><div class="col"><input id="ratioSeed" class="form-control" type="number" value="0" placeholder="Seed minutes"></div><div class="col"><select id="ratioAction" class="form-select"><option>stop</option><option>remove</option><option>pause</option></select></div><div class="col-auto"><button id="ratioSaveBtn" class="btn btn-primary"><i class="fa-solid fa-floppy-disk"></i> Save</button></div></div><div id="ratioManager"></div></div><div id="toolColumns" class="d-none"><div class="small text-muted mb-2">Choose columns visible in the torrent list.</div><div id="columnManager" class="column-manager"></div><div class="mt-3"><button id="saveColumnsBtn" class="btn btn-primary btn-sm"><i class="fa-solid fa-floppy-disk"></i> Save columns</button><button id="resetColumnsBtn" class="btn btn-outline-secondary btn-sm ms-2"><i class="fa-solid fa-rotate-left"></i> Reset</button></div></div><div id="toolSmart" class="d-none"><div class="surface-section smart-panel"><div class="smart-header"><div><div class="section-title mb-1"><i class="fa-solid fa-shuffle"></i> Smart Queue</div><div class="small text-muted">Automatic queue balancing for slow or stalled downloads.</div></div><div class="smart-header-actions"><button id="smartSaveBtn" class="btn btn-sm btn-primary"><i class="fa-solid fa-floppy-disk"></i> Save</button><button id="smartCheckBtn" class="btn btn-sm btn-success"><i class="fa-solid fa-play"></i> Check now</button></div></div><!-- UI note: Smart Queue settings use compact rows instead of isolated cards, so switches align with regular fields. --><div class="smart-settings-list"><div class="smart-setting-row smart-toggle-row"><div><label class="form-check-label" for="smartEnabled">Auto enabled</label><div class="form-text">Run Smart Queue automatically during polling.</div></div><div class="form-check form-switch"><input id="smartEnabled" class="form-check-input" type="checkbox"></div></div><div class="smart-setting-row smart-toggle-row"><div><label class="form-check-label" for="smartManageStopped">Use stopped torrents</label><div class="form-text">Off = only paused torrents are managed.</div></div><div class="form-check form-switch"><input id="smartManageStopped" class="form-check-input" type="checkbox"></div></div><div class="smart-input-grid"><label class="smart-input-field"><span>Target active downloads</span><input id="smartMaxActive" class="form-control form-control-sm" type="number" min="1" value="5"><small>Also raises rTorrent cap when lower.</small></label><label class="smart-input-field"><span>Stalled after seconds</span><input id="smartStalled" class="form-control form-control-sm" type="number" min="30" value="300"></label><label class="smart-input-field"><span>Min speed KiB/s</span><input id="smartMinSpeed" class="form-control form-control-sm" type="number" min="0" value="1"></label><label class="smart-input-field"><span>Min seeds</span><input id="smartMinSeeds" class="form-control form-control-sm" type="number" min="0" value="1"></label></div></div><div class="smart-actions mt-3"><button id="smartExcludeSelectedBtn" class="btn btn-sm btn-outline-warning"><i class="fa-solid fa-ban"></i> Exclude selected</button><button id="smartIncludeSelectedBtn" class="btn btn-sm btn-outline-secondary"><i class="fa-solid fa-rotate-left"></i> Include selected</button><span class="small text-muted">Torrents excluded below are ignored by Smart Queue and continue normally.</span></div><div id="smartManager" class="mt-3"></div><div class="section-title mt-3">Last operations</div><div id="smartHistory" class="mt-2"></div></div></div><div id="toolAutomations" class="d-none"><div class="surface-section"><div class="section-title"><i class="fa-solid fa-wand-magic-sparkles"></i> Automations / rules</div><div class="small text-muted mb-2">Create simple if/then rules. They run automatically every poll cycle group and can also be checked manually.</div><div class="automation-form-grid"><input id="autoName" class="form-control" placeholder="Rule name, e.g. Move completed movies"><label class="form-check form-switch"><input id="autoEnabled" class="form-check-input" type="checkbox" checked><span class="form-check-label">Enabled</span></label><input id="autoCooldown" class="form-control" type="number" min="0" value="60" title="Cooldown minutes"><select id="autoConditionType" class="form-select"><option value="completed">When completed</option><option value="no_seeds">When seeds are low for time</option><option value="ratio_gte">When ratio is at least</option><option value="label_missing">When label is missing</option><option value="label_has">When label exists</option><option value="status">When status equals</option><option value="path_contains">When path contains</option></select><input id="autoCondSeeds" data-auto-cond="no_seeds" class="form-control" type="number" min="0" value="0" placeholder="Seeds <="><input id="autoCondMinutes" data-auto-cond="no_seeds" class="form-control" type="number" min="0" value="30" placeholder="Minutes"><input id="autoCondRatio" data-auto-cond="ratio_gte" class="form-control" type="number" step="0.1" value="1" placeholder="Ratio"><input id="autoCondLabel" data-auto-cond="label_missing,label_has" class="form-control" placeholder="Label"><select id="autoCondStatus" data-auto-cond="status" class="form-select"><option>Downloading</option><option>Seeding</option><option>Paused</option><option>Stopped</option><option>Checking</option></select><input id="autoCondText" data-auto-cond="path_contains" class="form-control" placeholder="Path text"><select id="autoEffectType" class="form-select"><option value="add_label">Add label</option><option value="remove_label">Remove label</option><option value="set_labels">Set labels</option><option value="move">Move to path</option><option value="pause">Pause</option><option value="stop">Stop</option><option value="start">Start</option><option value="resume">Resume</option><option value="recheck">Recheck</option></select><input id="autoEffectLabel" data-auto-effect="add_label,remove_label" class="form-control" placeholder="Label"><input id="autoEffectLabels" data-auto-effect="set_labels" class="form-control" placeholder="Labels separated by comma"><div data-auto-effect="move" class="input-group"><input id="autoEffectPath" class="form-control" placeholder="Target path, empty = rTorrent default"><button class="btn btn-outline-secondary browse-path" data-target="autoEffectPath" type="button"><i class="fa-solid fa-folder-open"></i></button></div><div class="d-flex gap-2"><button id="automationSaveBtn" class="btn btn-sm btn-primary"><i class="fa-solid fa-floppy-disk"></i> Save rule</button><button id="automationCheckBtn" class="btn btn-sm btn-success"><i class="fa-solid fa-play"></i> Check now</button></div></div><div class="section-title mt-3">Rules</div><div id="automationManager"></div><div class="section-title mt-3">History</div><div id="automationHistory"></div></div></div><div id="toolRss" class="d-none"><div class="row g-2 mb-3"><div class="col"><input id="rssName" class="form-control" placeholder="Feed name"></div><div class="col"><input id="rssUrl" class="form-control" placeholder="https://.../feed.rss"></div><div class="col-auto"><button id="rssFeedBtn" class="btn btn-primary"><i class="fa-solid fa-plus"></i> Add feed</button></div></div><div class="row g-2 mb-3"><div class="col"><input id="rssRuleName" class="form-control" placeholder="Rule name"></div><div class="col"><input id="rssPattern" class="form-control" placeholder="Regex pattern"></div><div class="col"><input id="rssPath" class="form-control" placeholder="Save path"></div><div class="col"><input id="rssLabel" class="form-control" placeholder="Label"></div><div class="col-auto"><button id="rssRuleBtn" class="btn btn-outline-primary"><i class="fa-solid fa-plus"></i> Add rule</button></div><div class="col-auto"><button id="rssCheckBtn" class="btn btn-success"><i class="fa-solid fa-play"></i> Check now</button></div></div><div id="rssManager"></div></div></div></div></div></div>
<div class="modal fade" id="trafficModal" tabindex="-1"><div class="modal-dialog modal-xl"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="fa-solid fa-chart-column"></i> Transfer history</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="d-flex gap-2 mb-3 flex-wrap"><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="15m"><i class="fa-solid fa-clock"></i> 15m</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="1h"><i class="fa-solid fa-clock"></i> 1h</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="3h"><i class="fa-solid fa-clock"></i> 3h</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="6h"><i class="fa-solid fa-clock"></i> 6h</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="24h"><i class="fa-solid fa-calendar-day"></i> 24h</button><button class="btn btn-sm btn-primary traffic-range" data-range="7d"><i class="fa-solid fa-calendar-week"></i> 7d</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="30d"><i class="fa-solid fa-calendar"></i> 30d</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="90d"><i class="fa-solid fa-calendar"></i> 90d</button></div><div class="history-grid"><div class="history-card"><div class="history-title">Transferred data</div><canvas id="trafficHistoryChart"></canvas></div><div class="history-card"><div class="history-title">Speed trend</div><canvas id="trafficSpeedChart"></canvas></div></div><div id="trafficHistoryInfo" class="small text-muted mt-2"></div></div></div></div></div>
<div class="modal fade" id="aboutModal" tabindex="-1" aria-labelledby="aboutModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content about-modal-content">
<div class="modal-header">
<h5 id="aboutModalLabel" class="modal-title"><i class="fa-solid fa-robot"></i> About pyTorrent</h5>
<button class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="about-hero">
<div class="about-logo"><i class="fa-solid fa-robot"></i></div>
<div>
<h6>pyTorrent</h6>
<p>Lightweight web panel for rTorrent management.</p>
</div>
</div>
<dl class="about-list">
<div><dt>License</dt><dd>Open source</dd></div>
<div><dt>Author</dt><dd>linuxiarz.pl</dd></div>
<div><dt>Backend</dt><dd>Python, Flask, Flask-SocketIO</dd></div>
<div><dt>Frontend</dt><dd>Bootstrap, vanilla JavaScript, Font Awesome</dd></div>
<div><dt>Runtime</dt><dd>Gunicorn compatible, rTorrent over SCGI</dd></div>
</dl>
</div>
</div>
</div>
</div>
<div id="toastHost" class="toast-host"></div>
<script src="{{ frontend_asset_url('socket_io_js') }}"></script>
<script src="{{ frontend_asset_url('bootstrap_js') }}"></script>
<script>window.PYTORRENT = {authEnabled: {{ 1 if auth_enabled else 0 }}, currentUser: {% if current_user %}{{ current_user | tojson }}{% else %}null{% endif %}, activeProfile: {{ active_profile.id if active_profile else 'null' }}, tableColumns: {{ (prefs.table_columns_json or '{}') | safe }}, peersRefreshSeconds: {{ prefs.peers_refresh_seconds if prefs else 0 }}, portCheckEnabled: {{ 1 if prefs and prefs.port_check_enabled else 0 }}, bootstrapTheme: {{ (prefs.bootstrap_theme if prefs and prefs.bootstrap_theme else 'default') | tojson }}, fontFamily: {{ (prefs.font_family if prefs and prefs.font_family else 'default') | tojson }}, footerItems: {{ (prefs.footer_items_json or '{}') | safe }}, bootstrapThemes: {{ bootstrap_themes | tojson }}, bootstrapThemeUrls: { {% for key in bootstrap_themes.keys() %}{{ key | tojson }}: {{ bootstrap_theme_url(key) | tojson }}{% if not loop.last %}, {% endif %}{% endfor %} }, fontFamilies: {{ font_families | tojson }}};</script>
<script src="{{ static_url('app.js') }}"></script>
</body></html>