Ensimmäinen commit

This commit is contained in:
Tontze
2026-06-08 19:44:24 +03:00
commit 8b23166a51
11 changed files with 563 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
ladatut_torrentit.txt
*.old
Binary file not shown.
+137
View File
@@ -0,0 +1,137 @@
from flask import Flask, render_template, jsonify, request
import subprocess
import threading
import time
import os
import logging
import torrent_vahti
app = Flask(__name__)
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
vahti_prosessia = None
loki_sisalto = []
def lue_lokit(process):
global loki_sisalto
for line in iter(process.stdout.readline, ''):
if line:
loki_sisalto.append(line)
if len(loki_sisalto) > 500:
loki_sisalto.pop(0)
def suorita_haku_prosessi():
global vahti_prosessia
if vahti_prosessia is None or vahti_prosessia.poll() is not None:
loki_sisalto.append(f"\n--- Haku käynnistetty: {time.strftime('%H:%M:%S')} ---\n")
vahti_prosessia = subprocess.Popen(
['python', '-u', 'torrent_vahti.py'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
t = threading.Thread(target=lue_lokit, args=(vahti_prosessia,))
t.daemon = True
t.start()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/status')
def status():
kaikki_data = torrent_vahti.lataa_kaikki_data()
loki_teksti = ''.join(loki_sisalto)
try:
with open(torrent_vahti.LOKI_TIEDOSTO, "r", encoding="utf-8") as f:
loki_torrentit = f.read()
except:
loki_torrentit = "Historiaa ei löytynyt."
return jsonify({
'status': 'ok',
'hakukohteet': kaikki_data.get("hakukohteet", []),
'asetukset': kaikki_data.get("asetukset", {}),
'indeksorit': kaikki_data.get("indeksorit", []),
'loki_skripti': loki_teksti,
'loki_torrentit': loki_torrentit
})
@app.route('/tallenna_asetukset', methods=['POST'])
def tallenna_asetukset():
data = request.json
kaikki_data = torrent_vahti.lataa_kaikki_data()
kaikki_data["asetukset"] = {
"jackett_url": data.get('jackett_url', ''),
"jackett_api_key": data.get('jackett_api_key', ''),
"qbit_url": data.get('qbit_url', ''),
"qbit_user": data.get('qbit_user', 'admin'),
"qbit_pass": data.get('qbit_pass', '')
}
torrent_vahti.tallenna_kaikki_data(kaikki_data)
return jsonify({'status': 'ok'})
@app.route('/paivita_indeksorit', methods=['POST'])
def paivita_indeksorit():
uusi_lista = request.json.get("indeksorit", [])
kaikki_data = torrent_vahti.lataa_kaikki_data()
kaikki_data["indeksorit"] = uusi_lista
torrent_vahti.tallenna_kaikki_data(kaikki_data)
return jsonify({'status': 'ok'})
@app.route('/lisaa_kohde', methods=['POST'])
def lisaa_kohde():
data = request.json
kaikki_data = torrent_vahti.lataa_kaikki_data()
kaikki_data["hakukohteet"].append({
"jackett_haku": data['jackett_haku'],
"kohdekansio": data['kohdekansio']
})
torrent_vahti.tallenna_kaikki_data(kaikki_data)
return jsonify({'status': 'ok'})
@app.route('/poista_kohde/<int:index>', methods=['DELETE'])
def poista_kohde(index):
kaikki_data = torrent_vahti.lataa_kaikki_data()
if 0 <= index < len(kaikki_data["hakukohteet"]):
kaikki_data["hakukohteet"].pop(index)
torrent_vahti.tallenna_kaikki_data(kaikki_data)
return jsonify({'status': 'ok'})
@app.route('/poista_historiarivi', methods=['POST'])
def poista_historiarivi():
data = request.json
rivi_poistettavaksi = data.get('rivi')
if os.path.exists(torrent_vahti.LOKI_TIEDOSTO):
with open(torrent_vahti.LOKI_TIEDOSTO, "r", encoding="utf-8") as f:
rivit = f.readlines()
uudet_rivit = [r for r in rivit if r.strip() != rivi_poistettavaksi.strip()]
with open(torrent_vahti.LOKI_TIEDOSTO, "w", encoding="utf-8") as f:
f.writelines(uudet_rivit)
return jsonify({'status': 'ok'})
return jsonify({'status': 'error'}), 404
@app.route('/suorita_haku', methods=['POST'])
def suorita_haku():
suorita_haku_prosessi()
return jsonify({'status': 'ok'})
@app.route('/tyhjenna_loki', methods=['POST'])
def tyhjenna_loki():
if os.path.exists(torrent_vahti.LOKI_TIEDOSTO):
os.remove(torrent_vahti.LOKI_TIEDOSTO)
return jsonify({'status': 'ok'})
if __name__ == '__main__':
def automaattinen_ajastin():
while True:
time.sleep(900)
suorita_haku_prosessi()
ajastin_saie = threading.Thread(target=automaattinen_ajastin)
ajastin_saie.daemon = True
ajastin_saie.start()
app.run(host='0.0.0.0', port=5000)
+15
View File
@@ -0,0 +1,15 @@
services:
torrent-vahti:
image: python:3.11-slim
container_name: torrent-vahti-ajo
restart: unless-stopped
volumes:
- /mnt/media/admin/motorsports:/app
- /mnt/media/media/Downloads/torrent:/watch
working_dir: /app
environment:
- TZ=Europe/Helsinki
# LISÄTTY lxml PAKETTI PIP INSTALLIIN:
command: >
sh -c "pip install --no-cache-dir requests beautifulsoup4 lxml &&
while true; do python torrent_vahti.py; sleep 1800; done"
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY templates/ ./templates/
COPY torrent_vahti.py .
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
Executable
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# 1. Lisätään kaikki muutetut tiedostot
git add .
# 2. Kysytään commit-viestiä käyttäjältä
echo "Syötä commit-kommentti:"
read commit_viesti
# 3. Tehdään commit
git commit -m "$commit_viesti"
# 4. Pushataan muutokset
git push
echo "Päivitys valmis!"
+43
View File
@@ -0,0 +1,43 @@
{
"indeksorit": [
"knaben",
"torrentgalaxyclone",
"therarbg",
"thepiratebay",
"torrentdownload",
"limetorrents"
],
"hakukohteet": [
{
"jackett_haku": "motogp 2026 720p jff",
"regex_suodatin": "motogp.*?2026.*?720p.*?jff.*?",
"kohdekansio": "MotoGP"
},
{
"jackett_haku": "motogp 2026 tntsportshd 1080p",
"regex_suodatin": "motogp.*?2026.*?tntsportshd.*?1080p.*?",
"kohdekansio": "MotoGP"
},
{
"jackett_haku": "Formula 1 2026 1080p SS",
"regex_suodatin": "\tFormula.*?1.*?2026.*?1080p.*?SS.*?",
"kohdekansio": "Formula1"
},
{
"jackett_haku": "Formula 1 2026 SkyF1HD 1080p",
"regex_suodatin": "(?i)Formula.?1.*2026.*SkyF1HD.*1080p",
"kohdekansio": "Formula1"
},
{
"jackett_haku": "Rio hamasaki",
"kohdekansio": "All"
}
],
"asetukset": {
"jackett_url": "http://192.168.1.40:9117",
"jackett_api_key": "gtqa5zn9ik93jfyj48y29ot3fayawvo6",
"qbit_url": "http://192.168.1.40:8080/",
"qbit_user": "admin",
"qbit_pass": "qbt_uFxc7gqD7sJsBk9FJdH38bLIyapr"
}
}
+4
View File
@@ -0,0 +1,4 @@
Flask==3.0.3
requests==2.31.0
beautifulsoup4==4.12.3
lxml==5.1.0
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+177
View File
@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Torrent Vahti Ohjauspaneeli</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #f8f9fa; font-family: system-ui, -apple-system, sans-serif; }
.card { border: none; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.nav-tabs .nav-link { color: #495057; font-weight: 500; }
.nav-tabs .nav-link.active { font-weight: 600; }
#loki-sisalto, #skripti-loki {
background-color: #212529;
color: #f8f9fa;
padding: 15px;
border-radius: 5px;
font-size: 0.85rem;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
}
</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark mb-4">
<div class="container">
<span class="navbar-brand mb-0 h1">🤖 Torrent Vahti Ohjauspaneeli</span>
<span id="tila-badge" class="badge bg-secondary">Ladataan...</span>
</div>
</nav>
<div class="container mb-5">
<div class="row">
<div class="col-100">
<ul class="nav nav-tabs mb-4" id="paneeliTabs" role="tablist">
<li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#haut-pane" type="button">📋 Hakujen Hallinta</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#historia-pane" type="button">⏳ Lataushistoria</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#asetukset-pane" type="button">⚙️ Järjestelmäasetukset</button></li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="haut-pane">
<div class="card p-4 mb-4">
<h5> Lisää uusi haku</h5>
<form id="lisaa-form" class="row g-3">
<div class="col-md-6"><input type="text" id="haku-sana" class="form-control form-control-sm" placeholder="Haku" required></div>
<input type="hidden" id="haku-regex" value=".*">
<div class="col-md-5"><input type="text" id="haku-kansio" class="form-control form-control-sm" placeholder="Kansio" required></div>
<div class="col-md-1"><button type="submit" class="btn btn-success btn-sm w-100">Lisää</button></div>
</form>
</div>
<div class="card p-4">
<h5>Aktiiviset haut</h5>
<table class="table table-hover align-middle"><tbody id="hakukohteet-lista"></tbody></table>
</div>
</div>
<div class="tab-pane fade" id="historia-pane">
<div class="card p-4">
<h5>Ladatut Torrentit</h5>
<div class="d-flex gap-2 mb-3">
<input type="text" id="historia-haku" class="form-control form-control-sm" placeholder="Suodata nimen mukaan..." oninput="paivitaNakyma()">
<button id="tyhjenna-loki-btn" class="btn btn-outline-danger btn-sm text-nowrap">Tyhjennä historia</button>
</div>
<div id="loki-sisalto">Ladataan...</div>
</div>
</div>
<div class="tab-pane fade" id="asetukset-pane">
<div class="card p-4">
<form id="asetukset-form">
<h6 class="fw-bold mb-3 text-primary">📡 Jackett Asetukset</h6>
<div class="mb-3"><input type="url" id="cfg-url" class="form-control form-control-sm" placeholder="Jackett URL"></div>
<div class="mb-4"><input type="text" id="cfg-apikey" class="form-control form-control-sm" placeholder="Jackett API-avain"></div>
<hr class="my-4">
<h6 class="fw-bold mb-3 text-success">🐾 qBittorrent Asetukset</h6>
<div class="mb-2"><input type="url" id="cfg-qbiturl" class="form-control form-control-sm" placeholder="Web UI URL"></div>
<div class="row mb-2">
<div class="col-sm-6"><input type="text" id="cfg-qbituser" class="form-control form-control-sm" placeholder="Käyttäjätunnus"></div>
<div class="col-sm-6"><input type="text" id="cfg-qbitpass" class="form-control form-control-sm" placeholder="Salasana/API"></div>
</div>
<button type="submit" class="btn btn-primary btn-sm mt-3">Tallenna kaikki asetukset</button>
</form>
<hr class="mt-5">
<h6 class="fw-bold mb-3 text-warning">🔍 Jackett Indexerit</h6>
<div class="input-group mb-3" style="max-width: 400px;">
<input type="text" id="uusi-indexer" class="form-control form-control-sm" placeholder="Indexer ID">
<button type="button" onclick="lisaaIndexer()" class="btn btn-outline-secondary btn-sm">Lisää</button>
</div>
<ul id="indexer-lista" class="list-group mb-3" style="max-width: 400px;"></ul>
</div>
</div>
</div>
<div class="card p-4 mt-4 bg-dark text-light">
<div class="d-flex justify-content-between"><h5>⚡ Järjestelmäloki</h5><button id="suorita-haku-btn" class="btn btn-success btn-sm">Suorita haku</button></div>
<pre id="skripti-loki">Ladataan...</pre>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function rullaaLokiAlas() {
const lokit = ['loki-sisalto', 'skripti-loki'];
lokit.forEach(id => {
const el = document.getElementById(id);
el.scrollTop = el.scrollHeight;
});
}
function paivitaNakyma() {
fetch('/status').then(res => res.json()).then(data => {
document.getElementById('tila-badge').innerText = "Valmis";
document.getElementById('tila-badge').className = "badge bg-success";
const lista = document.getElementById('hakukohteet-lista');
lista.innerHTML = data.hakukohteet.map((k, i) => `<tr><td>${k.jackett_haku}</td><td>${k.kohdekansio}</td><td class="text-end"><button onclick="poistaKohde(${i})" class="btn btn-link text-danger btn-sm p-0">Poista</button></td></tr>`).join('');
const hakuTermi = document.getElementById('historia-haku').value.toLowerCase();
const lokiDiv = document.getElementById('loki-sisalto');
const kaikkiRivit = (data.loki_torrentit || '').split('\n').filter(r => r.trim() !== '');
lokiDiv.innerHTML = kaikkiRivit
.filter(r => r.toLowerCase().includes(hakuTermi))
.map((r) => {
return `<div class="d-flex justify-content-between border-bottom py-1">
<span class="font-monospace small text-truncate" style="max-width: 80%;" title="${r}">${r}</span>
<button onclick="poistaHist('${r.replace(/'/g, "\\'")}')" class="btn btn-outline-danger btn-sm py-0">Poista</button>
</div>`;
}).join('');
const iList = document.getElementById('indexer-lista');
iList.innerHTML = (data.indeksorit || []).map((idx, i) => `<li class="list-group-item d-flex justify-content-between py-1">${idx}<button onclick="poistaIndexer(${i})" class="btn btn-danger btn-sm py-0">Poista</button></li>`).join('');
document.getElementById('skripti-loki').innerText = data.loki_skripti;
document.getElementById('cfg-url').value = data.asetukset.jackett_url || '';
document.getElementById('cfg-apikey').value = data.asetukset.jackett_api_key || '';
document.getElementById('cfg-qbiturl').value = data.asetukset.qbit_url || '';
document.getElementById('cfg-qbituser').value = data.asetukset.qbit_user || '';
document.getElementById('cfg-qbitpass').value = data.asetukset.qbit_pass || '';
rullaaLokiAlas();
});
}
function poistaHist(riviTeksti) {
if(confirm("Poistetaanko tämä rivi historiasta?")) {
fetch('/poista_historiarivi', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ rivi: riviTeksti })
}).then(() => paivitaNakyma());
}
}
function lisaaIndexer() { const val = document.getElementById('uusi-indexer').value; if(!val) return; fetch('/status').then(r => r.json()).then(d => { let l = d.indeksorit || []; l.push(val); fetch('/paivita_indeksorit', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({indeksorit:l})}).then(() => paivitaNakyma()); }); }
function poistaIndexer(i) { fetch('/status').then(r => r.json()).then(d => { let l = d.indeksorit; l.splice(i, 1); fetch('/paivita_indeksorit', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({indeksorit:l})}).then(() => paivitaNakyma()); }); }
function poistaKohde(i) { fetch(`/poista_kohde/${i}`, {method:'DELETE'}).then(() => paivitaNakyma()); }
document.getElementById('lisaa-form').addEventListener('submit', (e) => {
e.preventDefault();
fetch('/lisaa_kohde', {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({
jackett_haku: document.getElementById('haku-sana').value,
regex_suodatin: document.getElementById('haku-regex').value,
kohdekansio: document.getElementById('haku-kansio').value
})
}).then(() => { e.target.reset(); paivitaNakyma(); });
});
document.getElementById('asetukset-form').addEventListener('submit', (e) => { e.preventDefault(); fetch('/tallenna_asetukset', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({jackett_url:document.getElementById('cfg-url').value, jackett_api_key:document.getElementById('cfg-apikey').value, qbit_url:document.getElementById('cfg-qbiturl').value, qbit_user:document.getElementById('cfg-qbituser').value, qbit_pass:document.getElementById('cfg-qbitpass').value})}).then(() => paivitaNakyma()); });
document.getElementById('suorita-haku-btn').addEventListener('click', () => fetch('/suorita_haku', {method:'POST'}));
document.getElementById('tyhjenna-loki-btn').addEventListener('click', () => fetch('/tyhjenna_loki', {method:'POST'}).then(() => paivitaNakyma()));
paivitaNakyma(); setInterval(paivitaNakyma, 5000);
</script>
</body>
</html>
+155
View File
@@ -0,0 +1,155 @@
import os
import re
import time
import requests
import json
from bs4 import BeautifulSoup
SKRIPTIN_KANSIO = os.path.dirname(os.path.abspath(__file__))
LOKI_TIEDOSTO = os.path.join(SKRIPTIN_KANSIO, "ladatut_torrentit.txt")
JSON_TIEDOSTO = os.path.join(SKRIPTIN_KANSIO, "hakulista.json")
def lataa_kaikki_data():
oletus_data = {
"asetukset": {
"jackett_url": "http://192.168.1.40:9117",
"jackett_api_key": "",
"qbit_url": "http://172.16.0.1:8080",
"qbit_user": "admin",
"qbit_pass": ""
},
"indeksorit": ["knaben", "torrentgalaxyclone", "therarbg", "uindex"],
"hakukohteet": []
}
if os.path.exists(JSON_TIEDOSTO):
try:
with open(JSON_TIEDOSTO, "r", encoding="utf-8") as f:
data = json.load(f)
if "asetukset" not in data:
data["asetukset"] = oletus_data["asetukset"]
return data
except Exception as e:
print(f"Virhe JSON-luvussa: {e}")
tallenna_kaikki_data(oletus_data)
return oletus_data
def tallenna_kaikki_data(data):
with open(JSON_TIEDOSTO, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def lataa_jo_ladatut():
if os.path.exists(LOKI_TIEDOSTO):
with open(LOKI_TIEDOSTO, "r", encoding="utf-8") as f:
return set(line.strip() for line in f if line.strip())
return set()
def tallenna_lokiin(linkki):
with open(LOKI_TIEDOSTO, "a", encoding="utf-8") as f:
f.write(linkki + "\n")
def hae_jackettista(jackett_url, api_key, indeksori, hakusana):
url = f"{jackett_url}/api/v2.0/indexers/{indeksori}/results/torznab/api"
params = {"apikey": api_key, "t": "search", "q": hakusana}
try:
res = requests.get(url, params=params, timeout=15)
res.raise_for_status()
soup = BeautifulSoup(res.text, 'xml')
loydetyt = []
for item in soup.find_all('item'):
title = item.find('title')
title_text = title.text if title else ""
enclosure = item.find('enclosure', url=True)
linkki = enclosure['url'] if enclosure and enclosure['url'].startswith('magnet:') else None
if not linkki:
link_tag = item.find('link')
if link_tag and link_tag.text.startswith('magnet:'):
linkki = link_tag.text
if linkki and title_text:
loydetyt.append((title_text, linkki))
return loydetyt
except Exception as e:
print(f" [Jackett Virhe sivustolla {indeksori}] {e}")
return []
def qbit_lisaa_torrent_api_keylla(qbit_url, api_key, magnet_linkki, kategoria):
pohja_url = qbit_url.rstrip('/')
add_url = f"{pohja_url}/api/v2/torrents/add"
headers = {
"X-API-Key": api_key,
"Authorization": f"Bearer {api_key}",
"Referer": f"{pohja_url}/"
}
data = {
"urls": magnet_linkki,
"category": kategoria
}
try:
res = requests.post(add_url, data=data, headers=headers, timeout=10)
return res.status_code
except Exception as e:
print(f" [qBittorrent] Yhteysvirhe: {e}")
return 500
def tarkista_ja_lataa():
print(f"--- ALOITETAAN ERITELTY JACKETT & QBITTORRENT API-HAKU ({time.strftime('%Y-%m-%d %H:%M:%S')}) ---")
data = lataa_kaikki_data()
ladatut = lataa_jo_ladatut()
jackett_url = data["asetukset"].get("jackett_url", "")
api_key = data["asetukset"].get("jackett_api_key", "")
qbit_url = data["asetukset"].get("qbit_url", "")
qbit_api_key = data["asetukset"].get("qbit_pass", "")
indeksori_lista = data["indeksorit"]
hakulista = data["hakukohteet"]
if not api_key or not qbit_url or not qbit_api_key:
print(" [Virhe] Asetuksia puuttuu!")
return
for kohde in hakulista:
haku = kohde["jackett_haku"]
hakusanat = haku.lower().split()
kategoria = kohde["kohdekansio"].strip()
kaikki_tulokset = []
for idx in indeksori_lista:
tulokset = hae_jackettista(jackett_url, api_key, idx, haku)
kaikki_tulokset.extend(tulokset)
uudet_kohteet = []
for nimi, linkki in list(set(kaikki_tulokset)):
if linkki in ladatut: continue
nimi_lower = nimi.lower()
viimeisin_indeksi = -1
loytyy_kaikki = True
for sana in hakusanat:
indeksi = nimi_lower.find(sana, viimeisin_indeksi + 1)
if indeksi == -1:
loytyy_kaikki = False
break
viimeisin_indeksi = indeksi
if loytyy_kaikki:
uudet_kohteet.append((nimi, linkki))
for nimi, linkki in uudet_kohteet:
status = qbit_lisaa_torrent_api_keylla(qbit_url, qbit_api_key, linkki, kategoria)
if status == 200 or status == 409:
tallenna_lokiin(linkki)
print(f" -> KÄSITELTY: {nimi}")
print("--- HAKUKIERROS VALMIS ---\n")
if __name__ == "__main__":
tarkista_ja_lataa()