Adds update notification feature

Implements update notifications by fetching the latest version from a remote source.
Displays a warning message on the home and setup pages if the current version is outdated.
The update check is performed on application startup.
This commit is contained in:
OusmBlueNinja
2025-06-03 23:19:44 -05:00
parent 2f23da0230
commit 1966af1ae3
4 changed files with 391 additions and 266 deletions

40
main.py
View File

@@ -6,6 +6,7 @@ import threading
import time
import datetime
from collections import Counter
import requests
app = Flask(__name__)
app.secret_key = 'change_this_secret'
@@ -37,14 +38,17 @@ qb = None
torrent_data, torrent_news, torrent_stats, bandwidth_history = [], [], {}, []
auto_resume_enabled = True
data_lock = threading.Lock()
local_version = "1.0.1"
remote_version = ""
import requests
def check_outdated():
global remote_version
url = "https://dock-it.dev/GigabiteStudios/BitManager/raw/branch/main/version.txt"
def check_outdated(local_version, url):
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
remote_version = response.text.strip()
if local_version != remote_version:
@@ -55,10 +59,10 @@ def check_outdated(local_version, url):
return False
except requests.RequestException as e:
print(f"❌ Failed to fetch version file: {e}")
return False
outdated = check_outdated("1.0.1", "https://dock-it.dev/GigabiteStudios/BitManager/raw/branch/main/version.txt")
outdated = check_outdated()
# qBittorrent polling
@@ -169,7 +173,14 @@ def setup():
db.session.add(config)
db.session.commit()
return redirect(url_for('register'))
return render_template('setup.html')
return render_template(
'setup.html',
outdated=outdated,
current_version=local_version,
latest_version=remote_version
)
@app.route('/test-connection', methods=['POST'])
def test_connection():
@@ -192,8 +203,7 @@ def login():
user = User.query.filter_by(username=request.form['username']).first()
if user and bcrypt.check_password_hash(user.password, request.form['password']):
session['user'] = user.username
if connect_to_qb():
threading.Thread(target=poll_torrents, daemon=True).start()
return redirect(url_for('home'))
return render_template("login.html", error="Invalid credentials")
return render_template("login.html")
@@ -218,8 +228,7 @@ def register():
db.session.add(new_user)
db.session.commit()
session['user'] = new_user.username
if connect_to_qb():
threading.Thread(target=poll_torrents, daemon=True).start()
return redirect(url_for('home'))
return render_template("register.html")
@@ -227,7 +236,12 @@ def register():
# Views
@app.route('/')
def home():
return render_template("home.html", outdated = outdated)
return render_template(
'home.html',
outdated=outdated,
current_version=local_version,
latest_version=remote_version
)
@app.route('/dashboard')
def dashboard():
@@ -325,4 +339,6 @@ def api_auto_resume():
if __name__ == '__main__':
with app.app_context():
db.create_all()
if connect_to_qb():
threading.Thread(target=poll_torrents, daemon=True).start()
app.run(debug=True, host='0.0.0.0', port=5000)

40
setup.bat Normal file
View File

@@ -0,0 +1,40 @@
@echo off
setlocal
echo Checking for Python...
python --version >nul 2>&1
if errorlevel 1 (
echo.
echo [ERROR] Python is not installed or not in PATH.
echo Please install Python 3.8 or later and try again.
pause
exit /b 1
)
echo Python is installed.
echo.
echo Ensuring pip is available...
python -m ensurepip --upgrade >nul 2>&1
echo Upgrading pip...
python -m pip install --upgrade pip
echo Installing requirements...
if exist requirements.txt (
python -m pip install -r requirements.txt
) else (
echo [ERROR] requirements.txt not found in the current directory.
pause
exit /b 1
)
echo.
echo ✅ Requirements installed successfully.
echo.
echo ▶️ Starting Flask app...
python app.py
endlocal

View File

@@ -1,268 +1,310 @@
<!DOCTYPE html>
<html>
<head>
<title>qBittorrent Overview</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
background: #121212;
color: #e0e0e0;
font-family: 'Segoe UI', sans-serif;
margin: 0;
padding: 2rem;
}
nav {
margin-bottom: 2rem;
}
nav a {
color: #80dfff;
margin-right: 1.5rem;
text-decoration: none;
font-weight: bold;
}
h1, h2 {
margin-top: 0;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.card {
background: #1e1e1e;
border-radius: 8px;
padding: 1rem;
text-align: center;
}
.card h3 {
margin: 0;
font-size: 1.8rem;
color: #4bc0c0;
}
.card p {
margin: 0.2rem;
color: #aaa;
}
.flex {
display: flex;
flex-wrap: wrap;
gap: 2rem;
}
.chart-container, .news-feed {
background: #1e1e1e;
padding: 1rem;
border-radius: 8px;
flex: 1;
min-width: 380px;
}
.news-feed {
max-height: 500px;
overflow-y: auto;
}
.event {
margin-bottom: 0.75rem;
padding-left: 0.5rem;
border-left: 4px solid #444;
}
.event .time {
font-size: 0.9rem;
color: #999;
}
.event .state {
font-weight: bold;
color: #00e676;
}
.toggle {
background: #222;
padding: 1rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.toggle label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: bold;
}
.toggle input {
transform: scale(1.3);
}
</style>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/dashboard">Dashboard</a>
<a href="/news">News</a>
<a href="/settings">Settings</a>
{% if outdated %}
<a>⚠️ You're running an outdated version!</div>
{% endif %}
</nav>
<h1>Overview</h1>
<div class="toggle">
<label>
<input type="checkbox" id="resume-toggle" />
Auto-resume errored torrents
</label>
</div>
<div class="grid" id="metrics"></div>
<div class="flex">
<div class="chart-container">
<h2>Torrent State Distribution</h2>
<canvas id="stateChart"></canvas>
</div>
<div class="chart-container">
<h2>Bandwidth Usage</h2>
<canvas id="bandwidthChart"></canvas>
</div>
<div class="news-feed" id="news-feed">
<h2>Status Feed</h2>
</div>
</div>
<script>
let stateChart, bandwidthChart;
async function loadStats() {
const res = await fetch('/api/stats');
const stats = await res.json();
const grid = document.getElementById('metrics');
grid.innerHTML = '';
const stateCounts = stats.states || {};
const avg = stats.average_progress || 0;
for (const [state, count] of Object.entries(stateCounts)) {
const div = document.createElement('div');
div.className = 'card';
div.innerHTML = `<h3>${count}</h3><p>${state}</p>`;
grid.appendChild(div);
<head>
<title>qBittorrent Overview</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
background: #121212;
color: #e0e0e0;
font-family: "Segoe UI", sans-serif;
margin: 0;
padding: 2rem;
}
const avgCard = document.createElement('div');
avgCard.className = 'card';
avgCard.innerHTML = `<h3>${avg}%</h3><p>Overall Progress</p>`;
grid.appendChild(avgCard);
updateStateChart(stateCounts);
}
function updateStateChart(stateData) {
const labels = Object.keys(stateData);
const values = Object.values(stateData);
const colors = ['#4bc0c0', '#36a2eb', '#9966ff', '#ff6384', '#ffcd56', '#7fdb7f'];
if (!stateChart) {
stateChart = new Chart(document.getElementById('stateChart'), {
type: 'pie',
data: {
labels: labels,
datasets: [{ data: values, backgroundColor: colors }]
},
options: { plugins: { legend: { labels: { color: '#eee' } } } }
});
} else {
stateChart.data.labels = labels;
stateChart.data.datasets[0].data = values;
stateChart.update();
nav {
margin-bottom: 2rem;
}
}
async function loadBandwidth() {
const res = await fetch('/api/bandwidth');
const history = await res.json();
const labels = history.map(h => h.time);
const down = history.map(h => h.download);
const up = history.map(h => h.upload);
nav a {
color: #80dfff;
margin-right: 1.5rem;
text-decoration: none;
font-weight: bold;
}
if (!bandwidthChart) {
bandwidthChart = new Chart(document.getElementById('bandwidthChart'), {
type: 'line',
data: {
labels,
datasets: [
{ label: 'Download KB/s', data: down, borderColor: '#4bc0c0', tension: 0.3 },
{ label: 'Upload KB/s', data: up, borderColor: '#ff6384', tension: 0.3 }
]
},
options: {
scales: {
x: { ticks: { color: '#aaa' } },
y: { ticks: { color: '#aaa' } }
h1,
h2 {
margin-top: 0;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.card {
background: #1e1e1e;
border-radius: 8px;
padding: 1rem;
text-align: center;
}
.card h3 {
margin: 0;
font-size: 1.8rem;
color: #4bc0c0;
}
.card p {
margin: 0.2rem;
color: #aaa;
}
.flex {
display: flex;
flex-wrap: wrap;
gap: 2rem;
}
.chart-container,
.news-feed {
background: #1e1e1e;
padding: 1rem;
border-radius: 8px;
flex: 1;
min-width: 380px;
}
.news-feed {
max-height: 500px;
overflow-y: auto;
}
.event {
margin-bottom: 0.75rem;
padding-left: 0.5rem;
border-left: 4px solid #444;
}
.event .time {
font-size: 0.9rem;
color: #999;
}
.event .state {
font-weight: bold;
color: #00e676;
}
.toggle {
background: #222;
padding: 1rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.toggle label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: bold;
}
.toggle input {
transform: scale(1.3);
}
.warning {
background: #2a1b1b;
color: #ff6f61;
padding: 0.75rem 1rem;
margin-top: 1rem;
border-left: 4px solid #ff6f61;
border-radius: 6px;
font-size: 0.9rem;
}
.warning strong {
color: #ffab91;
}
</style>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/dashboard">Dashboard</a>
<a href="/news">News</a>
<a href="/settings">Settings</a>
</nav>
{% if outdated %}
<div class="warning">
<strong>Outdated:</strong> You are running version
<b>{{ current_version }}</b>. The latest is <b>{{ latest_version }}</b>.
</div>
{% endif %}
<h1>Overview</h1>
<div class="toggle">
<label>
<input type="checkbox" id="resume-toggle" />
Auto-resume errored torrents
</label>
</div>
<div class="grid" id="metrics"></div>
<div class="flex">
<div class="chart-container">
<h2>Torrent State Distribution</h2>
<canvas id="stateChart"></canvas>
</div>
<div class="chart-container">
<h2>Bandwidth Usage</h2>
<canvas id="bandwidthChart"></canvas>
</div>
<div class="news-feed" id="news-feed">
<h2>Status Feed</h2>
</div>
</div>
<script>
let stateChart, bandwidthChart;
async function loadStats() {
const res = await fetch("/api/stats");
const stats = await res.json();
const grid = document.getElementById("metrics");
grid.innerHTML = "";
const stateCounts = stats.states || {};
const avg = stats.average_progress || 0;
for (const [state, count] of Object.entries(stateCounts)) {
const div = document.createElement("div");
div.className = "card";
div.innerHTML = `<h3>${count}</h3><p>${state}</p>`;
grid.appendChild(div);
}
const avgCard = document.createElement("div");
avgCard.className = "card";
avgCard.innerHTML = `<h3>${avg}%</h3><p>Overall Progress</p>`;
grid.appendChild(avgCard);
updateStateChart(stateCounts);
}
function updateStateChart(stateData) {
const labels = Object.keys(stateData);
const values = Object.values(stateData);
const colors = [
"#4bc0c0",
"#36a2eb",
"#9966ff",
"#ff6384",
"#ffcd56",
"#7fdb7f",
];
if (!stateChart) {
stateChart = new Chart(document.getElementById("stateChart"), {
type: "pie",
data: {
labels: labels,
datasets: [{ data: values, backgroundColor: colors }],
},
plugins: { legend: { labels: { color: '#eee' } } }
}
options: { plugins: { legend: { labels: { color: "#eee" } } } },
});
} else {
stateChart.data.labels = labels;
stateChart.data.datasets[0].data = values;
stateChart.update();
}
}
async function loadBandwidth() {
const res = await fetch("/api/bandwidth");
const history = await res.json();
const labels = history.map((h) => h.time);
const down = history.map((h) => h.download);
const up = history.map((h) => h.upload);
if (!bandwidthChart) {
bandwidthChart = new Chart(
document.getElementById("bandwidthChart"),
{
type: "line",
data: {
labels,
datasets: [
{
label: "Download KB/s",
data: down,
borderColor: "#4bc0c0",
tension: 0.3,
},
{
label: "Upload KB/s",
data: up,
borderColor: "#ff6384",
tension: 0.3,
},
],
},
options: {
scales: {
x: { ticks: { color: "#aaa" } },
y: { ticks: { color: "#aaa" } },
},
plugins: { legend: { labels: { color: "#eee" } } },
},
}
);
} else {
bandwidthChart.data.labels = labels;
bandwidthChart.data.datasets[0].data = down;
bandwidthChart.data.datasets[1].data = up;
bandwidthChart.update();
}
}
async function loadNews() {
const res = await fetch("/api/news");
const news = await res.json();
const feed = document.getElementById("news-feed");
feed.innerHTML = "<h2>Status Feed</h2>";
for (const event of news.slice(0, 15)) {
const div = document.createElement("div");
div.className = "event";
div.innerHTML = `<div><span class="state">${event.state}</span> — <strong>${event.name}</strong></div><div class="time">${event.time}</div>`;
feed.appendChild(div);
}
}
async function loadResumeSetting() {
const res = await fetch("/api/auto_resume");
const json = await res.json();
document.getElementById("resume-toggle").checked = json.enabled;
}
async function setResumeSetting(value) {
await fetch("/api/auto_resume", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: value }),
});
} else {
bandwidthChart.data.labels = labels;
bandwidthChart.data.datasets[0].data = down;
bandwidthChart.data.datasets[1].data = up;
bandwidthChart.update();
}
}
async function loadNews() {
const res = await fetch('/api/news');
const news = await res.json();
const feed = document.getElementById('news-feed');
feed.innerHTML = '<h2>Status Feed</h2>';
for (const event of news.slice(0, 15)) {
const div = document.createElement('div');
div.className = 'event';
div.innerHTML = `<div><span class="state">${event.state}</span> — <strong>${event.name}</strong></div><div class="time">${event.time}</div>`;
feed.appendChild(div);
document
.getElementById("resume-toggle")
.addEventListener("change", (e) => {
setResumeSetting(e.target.checked);
});
async function refresh() {
await Promise.all([loadStats(), loadBandwidth(), loadNews()]);
}
}
async function loadResumeSetting() {
const res = await fetch('/api/auto_resume');
const json = await res.json();
document.getElementById('resume-toggle').checked = json.enabled;
}
async function setResumeSetting(value) {
await fetch('/api/auto_resume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: value })
});
}
document.getElementById('resume-toggle').addEventListener('change', e => {
setResumeSetting(e.target.checked);
});
async function refresh() {
await Promise.all([loadStats(), loadBandwidth(), loadNews()]);
}
setInterval(refresh, 1000);
loadResumeSetting();
refresh();
</script>
</body>
setInterval(refresh, 1000);
loadResumeSetting();
refresh();
</script>
</body>
</html>

View File

@@ -126,11 +126,30 @@
color: #ccc;
font-size: 0.9rem;
}
.warning {
background: #2a1b1b;
color: #ff6f61;
padding: 0.75rem 1rem;
margin-top: 1rem;
border-left: 4px solid #ff6f61;
border-radius: 6px;
font-size: 0.9rem;
}
.warning strong {
color: #ffab91;
}
</style>
</head>
<body>
<div class="card">
<h2>qBittorrent Setup</h2>
{% if outdated %}
<div class="warning">
<strong>Outdated:</strong> You are running version <b>{{ current_version }}</b>. The latest is <b>{{ latest_version }}</b>.
</div>
{% endif %}
<form method="POST">
<label>Web UI URL:</label>
<input name="url" required value="http://192.168.0.236:8080/">
@@ -152,9 +171,12 @@
<span id="test-message"></span>
<div class="info" id="test-info"></div>
</div>
</div>
<script>
function testConnection() {
const url = document.querySelector('input[name="url"]').value;
const username = document.querySelector('input[name="username"]').value;
@@ -165,6 +187,9 @@
const info = document.getElementById('test-info');
const checkmark = document.querySelector('.checkmark');
const testBtn = document.getElementById('testBtn');
const outdated = document.getElementById('outdated-warning');
const currVer = document.getElementById('current-version');
const latestVer = document.getElementById('latest-version');
testBtn.disabled = true;
testBtn.innerHTML = '<span class="spinner"></span>';
@@ -174,6 +199,7 @@
msg.innerText = '';
checkmark.textContent = '';
info.innerText = '';
outdated.style.display = 'none';
fetch('/test-connection', {
method: 'POST',
@@ -188,6 +214,7 @@
checkmark.textContent = '✔';
msg.innerText = 'Connected successfully!';
info.innerHTML = `Version: <b>${data.version}</b><br>Port: <b>${new URL(url).port || '8080'}</b>`;
} else {
box.className = 'result-box error';
checkmark.textContent = '✖';