1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
*.log
|
||||
*.json
|
||||
*.csv
|
||||
*.toml
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"host": "192.168.1.1",
|
||||
"port": 8080,
|
||||
"username": "admin",
|
||||
"password": "admin",
|
||||
"log_level": "INFO",
|
||||
"log_path": "./qc.log",
|
||||
"protected_tag": "ipt",
|
||||
"non_protected_tag": "public",
|
||||
"age": 2419200,
|
||||
"minimum_age": 432000,
|
||||
"use_pushover": false,
|
||||
"use_log": true,
|
||||
"po_key": "",
|
||||
"po_token": "",
|
||||
"delete_torrents": false,
|
||||
"enable_dragnet": true,
|
||||
"dragnet_outfile": "./dragnet.csv"
|
||||
}
|
43
config.toml.example
Normal file
43
config.toml.example
Normal file
@ -0,0 +1,43 @@
|
||||
[qbittorrent]
|
||||
host = "192.168.4.11"
|
||||
port = 8085
|
||||
username = "jman"
|
||||
password = "nO^touchy@"
|
||||
|
||||
[logging]
|
||||
use_log = true
|
||||
log_level = "DEBUG"
|
||||
log_path = "./qc.log"
|
||||
|
||||
[app_tags]
|
||||
protected_tag = "ipt"
|
||||
non_protected_tag = "public"
|
||||
|
||||
[torrent]
|
||||
age = 2419200
|
||||
minimum_age = 432000
|
||||
delete_torrents = true
|
||||
|
||||
[pushover]
|
||||
use_pushover = false
|
||||
po_key = ""
|
||||
po_token = ""
|
||||
|
||||
[dragnet]
|
||||
enable_dragnet = true
|
||||
dragnet_outfile = "./orphaned.csv"
|
||||
|
||||
[ignored_categories]
|
||||
tech = "tech"
|
||||
books = "books"
|
||||
general = "general"
|
||||
|
||||
[ignored_domains]
|
||||
iptorrents-empirehost = "ssl.empirehost.me"
|
||||
iptorrents-stackoverflow = "localhost.stackoverflow.tech"
|
||||
iptorrents-bgp = "routing.bgp.technology"
|
||||
|
||||
[ignored_tags]
|
||||
first = "first"
|
||||
second = "second"
|
||||
third = "third"
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"example": "general"
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"iptorrents-empirehost": "ssl.empirehost.me",
|
||||
"iptorrents-stackoverflow": "localhost.stackoverflow.tech",
|
||||
"iptorrents-bgp": "routing.bgp.technology"
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"first":"first",
|
||||
"second":"second",
|
||||
"third":"third"
|
||||
}
|
234
pushover.py
Normal file
234
pushover.py
Normal file
@ -0,0 +1,234 @@
|
||||
# pushover 1.2
|
||||
#
|
||||
# Copyright (C) 2013-2018 Thibaut Horel <thibaut.horel@gmail.com>
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import time
|
||||
import requests
|
||||
|
||||
BASE_URL = "https://api.pushover.net/1/"
|
||||
MESSAGE_URL = BASE_URL + "messages.json"
|
||||
USER_URL = BASE_URL + "users/validate.json"
|
||||
SOUND_URL = BASE_URL + "sounds.json"
|
||||
RECEIPT_URL = BASE_URL + "receipts/"
|
||||
GLANCE_URL = BASE_URL + "glances.json"
|
||||
|
||||
|
||||
class RequestError(Exception):
|
||||
"""Exception which is raised when Pushover's API returns an error code.
|
||||
|
||||
The list of errors is stored in the :attr:`errors` attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, errors):
|
||||
Exception.__init__(self)
|
||||
self.errors = errors
|
||||
|
||||
def __str__(self):
|
||||
return "\n==> " + "\n==> ".join(self.errors)
|
||||
|
||||
|
||||
class Request(object):
|
||||
"""Base class to send a request to the Pushover server and check the return
|
||||
status code. The request is sent on instantiation and raises
|
||||
a :class:`RequestError` exception when the request is rejected.
|
||||
"""
|
||||
|
||||
def __init__(self, method, url, payload):
|
||||
files = {}
|
||||
if "attachment" in payload:
|
||||
files["attachment"] = payload["attachment"]
|
||||
del payload["attachment"]
|
||||
self.payload = payload
|
||||
self.files = files
|
||||
request = getattr(requests, method)(url, params=payload, files=files)
|
||||
self.answer = request.json()
|
||||
if 400 <= request.status_code < 500:
|
||||
raise RequestError(self.answer["errors"])
|
||||
|
||||
def __str__(self):
|
||||
return str(self.answer)
|
||||
|
||||
|
||||
class MessageRequest(Request):
|
||||
"""This class represents a message request to the Pushover API. You do not
|
||||
need to create it yourself, but the :func:`Pushover.message` function
|
||||
returns :class:`MessageRequest` objects.
|
||||
|
||||
The :attr:`answer` attribute contains a JSON representation of the answer
|
||||
made by the Pushover API. When sending a message with a priority of 2, you
|
||||
can poll the status of the notification with the :func:`poll` function.
|
||||
"""
|
||||
|
||||
params = {
|
||||
"expired": "expires_at",
|
||||
"called_back": "called_back_at",
|
||||
"acknowledged": "acknowledged_at",
|
||||
}
|
||||
|
||||
def __init__(self, payload):
|
||||
Request.__init__(self, "post", MESSAGE_URL, payload)
|
||||
self.status = {"done": True}
|
||||
if payload.get("priority", 0) == 2:
|
||||
self.url = RECEIPT_URL + self.answer["receipt"]
|
||||
self.status["done"] = False
|
||||
for param, when in MessageRequest.params.items():
|
||||
self.status[param] = False
|
||||
self.status[when] = 0
|
||||
|
||||
def poll(self):
|
||||
"""If the message request has a priority of 2, Pushover keeps sending
|
||||
the same notification until the client acknowledges it. Calling the
|
||||
:func:`poll` function fetches the status of the :class:`MessageRequest`
|
||||
object until the notifications either expires, is acknowledged by the
|
||||
client, or the callback url is reached. The status is available in the
|
||||
``status`` dictionary.
|
||||
|
||||
Returns ``True`` when the request has expired or been acknowledged and
|
||||
``False`` otherwise so that a typical handling of a priority-2
|
||||
notification can look like this::
|
||||
|
||||
request = p.message("Urgent!", priority=2, expire=120, retry=60)
|
||||
while not request.poll():
|
||||
# do something
|
||||
time.sleep(5)
|
||||
|
||||
print request.status
|
||||
"""
|
||||
if not self.status["done"]:
|
||||
r = Request("get", self.url + ".json", {"token": self.payload["token"]})
|
||||
for param, when in MessageRequest.params.items():
|
||||
self.status[param] = bool(r.answer[param])
|
||||
self.status[when] = int(r.answer[when])
|
||||
for param in ["acknowledged_by", "acknowledged_by_device"]:
|
||||
self.status[param] = r.answer[param]
|
||||
self.status["last_delivered_at"] = int(r.answer["last_delivered_at"])
|
||||
if any(self.status[param] for param in MessageRequest.params):
|
||||
self.status["done"] = True
|
||||
return self.status["done"]
|
||||
|
||||
def cancel(self):
|
||||
"""If the message request has a priority of 2, Pushover keeps sending
|
||||
the same notification until it either reaches its ``expire`` value or
|
||||
is aknowledged by the client. Calling the :func:`cancel` function
|
||||
cancels the notification early.
|
||||
"""
|
||||
if not self.status["done"]:
|
||||
return Request(
|
||||
"post", self.url + "/cancel.json", {"token": self.payload["token"]}
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class Pushover(object):
|
||||
"""This is the main class of the module. It represents a Pushover app and
|
||||
is tied to a unique API token.
|
||||
|
||||
* ``token``: Pushover API token
|
||||
"""
|
||||
|
||||
_SOUNDS = None
|
||||
message_keywords = [
|
||||
"title",
|
||||
"priority",
|
||||
"sound",
|
||||
"callback",
|
||||
"timestamp",
|
||||
"url",
|
||||
"url_title",
|
||||
"device",
|
||||
"retry",
|
||||
"expire",
|
||||
"html",
|
||||
"attachment",
|
||||
]
|
||||
glance_keywords = ["title", "text", "subtext", "count", "percent", "device"]
|
||||
|
||||
def __init__(self, token):
|
||||
self.token = token
|
||||
|
||||
@property
|
||||
def sounds(self):
|
||||
"""Return a dictionary of sounds recognized by Pushover and that can be
|
||||
used in a notification message.
|
||||
"""
|
||||
if not Pushover._SOUNDS:
|
||||
request = Request("get", SOUND_URL, {"token": self.token})
|
||||
Pushover._SOUNDS = request.answer["sounds"]
|
||||
return Pushover._SOUNDS
|
||||
|
||||
def verify(self, user, device=None):
|
||||
"""Verify that the `user` and optional `device` exist. Returns
|
||||
`None` when the user/device does not exist or a list of the user's
|
||||
devices otherwise.
|
||||
"""
|
||||
payload = {"user": user, "token": self.token}
|
||||
if device:
|
||||
payload["device"] = device
|
||||
try:
|
||||
request = Request("post", USER_URL, payload)
|
||||
except RequestError:
|
||||
return None
|
||||
else:
|
||||
return request.answer["devices"]
|
||||
|
||||
def message(self, user, message, **kwargs):
|
||||
"""Send `message` to the user specified by `user`. It is possible
|
||||
to specify additional properties of the message by passing keyword
|
||||
arguments. The list of valid keywords is ``title, priority, sound,
|
||||
callback, timestamp, url, url_title, device, retry, expire and html``
|
||||
which are described in the Pushover API documentation.
|
||||
|
||||
For convenience, you can simply set ``timestamp=True`` to set the
|
||||
timestamp to the current timestamp.
|
||||
|
||||
An image can be attached to a message by passing a file-like object
|
||||
to the `attachment` keyword argument.
|
||||
|
||||
This method returns a :class:`MessageRequest` object.
|
||||
"""
|
||||
|
||||
payload = {"message": message, "user": user, "token": self.token}
|
||||
for key, value in kwargs.items():
|
||||
if key not in Pushover.message_keywords:
|
||||
raise ValueError("{0}: invalid message parameter".format(key))
|
||||
elif key == "timestamp" and value is True:
|
||||
payload[key] = int(time.time())
|
||||
elif key == "sound" and value not in self.sounds:
|
||||
raise ValueError("{0}: invalid sound".format(value))
|
||||
else:
|
||||
payload[key] = value
|
||||
|
||||
return MessageRequest(payload)
|
||||
|
||||
def glance(self, user, **kwargs):
|
||||
"""Send a glance to the user. The default property is ``text``, as this
|
||||
is used on most glances, however a valid glance does not need to
|
||||
require text and can be constructed using any combination of valid
|
||||
keyword properties. The list of valid keywords is ``title, text,
|
||||
subtext, count, percent and device`` which are described in the
|
||||
Pushover Glance API documentation.
|
||||
|
||||
This method returns a :class:`GlanceRequest` object.
|
||||
"""
|
||||
payload = {"user": user, "token": self.token}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key not in Pushover.glance_keywords:
|
||||
raise ValueError("{0}: invalid glance parameter".format(key))
|
||||
else:
|
||||
payload[key] = value
|
||||
|
||||
return Request("post", GLANCE_URL, payload)
|
71
qbit-maid.py
71
qbit-maid.py
@ -1,6 +1,6 @@
|
||||
import qbittorrentapi
|
||||
import pushover
|
||||
from json import load
|
||||
from tomllib import load
|
||||
from qlist import *
|
||||
from qlogging import *
|
||||
from qprocess import *
|
||||
@ -15,44 +15,61 @@ class Qbt:
|
||||
"""Main object, should be calling functions from qlist.py, qlogging.py and qprocess.py"""
|
||||
# Open the config. Needs a json file with the data in config.json.example
|
||||
self.st = datetime.datetime.now()
|
||||
c = open('./config.json')
|
||||
c = open('./config.toml', 'rb')
|
||||
self.config = load(c)
|
||||
w = open('./ignored_categories.json')
|
||||
self.cat_whitelist = load(w)
|
||||
tg = open('./ignored_tags.json')
|
||||
self.ignored_tags = load(tg)
|
||||
# Create the api object
|
||||
|
||||
# # Create the api object
|
||||
self.qbt_client = qbittorrentapi.Client(
|
||||
host=self.config["host"],
|
||||
port=self.config["port"],
|
||||
username=self.config["username"],
|
||||
password=self.config["password"],
|
||||
# qbittorrent
|
||||
host=self.config["qbittorrent"]["host"],
|
||||
port=self.config["qbittorrent"]["port"],
|
||||
username=self.config["qbittorrent"]["username"],
|
||||
password=self.config["qbittorrent"]["password"],
|
||||
)
|
||||
# Create the logging and pushover objects
|
||||
self.tl = logging
|
||||
self.po = pushover
|
||||
self.ct = Counter
|
||||
self.cv = csv
|
||||
# Init config.json
|
||||
self.use_pushover = self.config["use_pushover"]
|
||||
self.use_log = self.config["use_log"]
|
||||
self.po_key = self.config["po_key"]
|
||||
self.po_token = self.config["po_token"]
|
||||
self.log_path = self.config["log_path"]
|
||||
self.log_level = self.config["log_level"]
|
||||
self.tracker_protected_tag = self.config["protected_tag"]
|
||||
self.tracker_non_protected_tag = self.config["non_protected_tag"]
|
||||
self.minimum_age = self.config["minimum_age"]
|
||||
self.age = self.config["age"]
|
||||
self.enable_dragnet = self.config["enable_dragnet"]
|
||||
self.dragnet_outfile = self.config["dragnet_outfile"]
|
||||
# Init config.toml
|
||||
|
||||
# logging
|
||||
self.use_log = self.config["logging"]["use_log"]
|
||||
self.log_path = self.config["logging"]["log_path"]
|
||||
self.log_level = self.config["logging"]["log_level"]
|
||||
|
||||
#app_tags
|
||||
self.tracker_protected_tag = self.config["app_tags"]["protected_tag"]
|
||||
self.tracker_non_protected_tag = self.config["app_tags"]["non_protected_tag"]
|
||||
|
||||
#torrent
|
||||
self.delete_torrents = self.config["torrent"]["delete_torrents"]
|
||||
self.minimum_age = self.config["torrent"]["minimum_age"]
|
||||
self.age = self.config["torrent"]["age"]
|
||||
|
||||
#pushover
|
||||
self.use_pushover = self.config["pushover"]["use_pushover"]
|
||||
self.po_key = self.config["pushover"]["po_key"]
|
||||
self.po_token = self.config["pushover"]["po_token"]
|
||||
|
||||
#dragnet
|
||||
self.enable_dragnet = self.config["dragnet"]["enable_dragnet"]
|
||||
self.dragnet_outfile = self.config["dragnet"]["dragnet_outfile"]
|
||||
|
||||
#ignored_categories
|
||||
self.cat_whitelist = self.config["ignored_categories"]
|
||||
|
||||
#ignored_domains
|
||||
self.tracker_whitelist = self.config["ignored_domains"]
|
||||
|
||||
#ignored_tags
|
||||
self.ignored_tags = self.config["ignored_domains"]
|
||||
|
||||
# Calling log and notify functions
|
||||
tor_log(self)
|
||||
tor_notify(self)
|
||||
self.t = time
|
||||
# Pulling domain names to treat carefully
|
||||
f = open('./ignored_domains.json')
|
||||
self.tracker_whitelist = load(f)
|
||||
self.tracker_list = []
|
||||
self.up_tor_counter = 0
|
||||
self.preme_tor_counter = 0
|
||||
@ -81,7 +98,7 @@ class Qbt:
|
||||
tor_processor(self)
|
||||
if self.use_log:
|
||||
print_processor(self)
|
||||
if self.config["delete_torrents"]:
|
||||
if self.delete_torrents:
|
||||
tor_delete(self)
|
||||
self.et = datetime.datetime.now()
|
||||
get_script_runtime(self)
|
||||
|
@ -10,11 +10,11 @@ def tor_notify(self):
|
||||
"""Seting up to use pushover, if self.use_pushover is set to true and
|
||||
if valid self.po_key and self.po_token is provided in the config file"""
|
||||
if self.use_pushover:
|
||||
self.poc = self.po.Client(self.po_key, api_token=self.po_token)
|
||||
self.poc = self.po.Pushover(self.po_token)
|
||||
|
||||
def tornotifytest(self):
|
||||
"""Used to make sure tornotify is working and messages are getting to the client"""
|
||||
self.poc.send_message("Test Message", title="qbit-maid")
|
||||
self.poc.message(self.po_key, "Test Message", title="qbit-maid")
|
||||
|
||||
def process_counts(self):
|
||||
self.c = self.ct()
|
||||
@ -33,7 +33,7 @@ def print_processor(self):
|
||||
|
||||
def tor_notify_summary(self):
|
||||
"""Main notification method when the app is used in an automated fashion"""
|
||||
self.poc.send_message(f" Total: {self.total_torrents}\n\
|
||||
self.poc.message(self.po_key, f" Total: {self.total_torrents}\n\
|
||||
Premature: {self.preme_tor_counter}\n\
|
||||
Ignored: {self.ignored_counter}\n\
|
||||
Protected: {self.c[self.tracker_protected_tag]}\n\
|
||||
|
@ -1,2 +1 @@
|
||||
python_pushover==0.4
|
||||
qbittorrent_api==2022.5.32
|
@ -1,21 +1,21 @@
|
||||
from qprocess import dragnet
|
||||
import csv
|
||||
import unittest
|
||||
|
||||
class TestDragnet(unittest.TestCase):
|
||||
def test_dragnet_sanity(self):
|
||||
self.cv = csv
|
||||
outfile = './test_outfile.csv'
|
||||
state = 'downloading'
|
||||
ratio = 1.05
|
||||
tags = 'ipt'
|
||||
added = 1
|
||||
age = 240000
|
||||
time = 123456
|
||||
thash = 'asfasdf23412adfqwer'
|
||||
tname = 'thisismynamehahahah'
|
||||
trname = 'https://localhost.stackoverflow.tech/317332f1c125bc9c1b9b14fb8e054908/announce'
|
||||
dragnet(self.cv,outfile,state,ratio,tags,added,age,time,thash,tname,trname)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from qprocess import dragnet
|
||||
import csv
|
||||
import unittest
|
||||
|
||||
class TestDragnet(unittest.TestCase):
|
||||
def test_dragnet_sanity(self):
|
||||
self.cv = csv
|
||||
outfile = './test_outfile.csv'
|
||||
state = 'downloading'
|
||||
ratio = 1.05
|
||||
tags = 'ipt'
|
||||
added = 1
|
||||
age = 240000
|
||||
time = 123456
|
||||
thash = 'asfasdf23412adfqwer'
|
||||
tname = 'thisismynamehahahah'
|
||||
trname = 'https://localhost.stackoverflow.tech/317332f1c125bc9c1b9b14fb8e054908/announce'
|
||||
dragnet(self.cv,outfile,state,ratio,tags,added,age,time,thash,tname,trname)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user