Fix saved tags #41

Merged
jonbranan merged 2 commits from fix-saved-tags into main 2023-03-15 13:42:24 -05:00
3 changed files with 269 additions and 261 deletions

View File

@ -1,234 +1,234 @@
# pushover 1.2 # pushover 1.2
# #
# Copyright (C) 2013-2018 Thibaut Horel <thibaut.horel@gmail.com> # Copyright (C) 2013-2018 Thibaut Horel <thibaut.horel@gmail.com>
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import time import time
import requests import requests
BASE_URL = "https://api.pushover.net/1/" BASE_URL = "https://api.pushover.net/1/"
MESSAGE_URL = BASE_URL + "messages.json" MESSAGE_URL = BASE_URL + "messages.json"
USER_URL = BASE_URL + "users/validate.json" USER_URL = BASE_URL + "users/validate.json"
SOUND_URL = BASE_URL + "sounds.json" SOUND_URL = BASE_URL + "sounds.json"
RECEIPT_URL = BASE_URL + "receipts/" RECEIPT_URL = BASE_URL + "receipts/"
GLANCE_URL = BASE_URL + "glances.json" GLANCE_URL = BASE_URL + "glances.json"
class RequestError(Exception): class RequestError(Exception):
"""Exception which is raised when Pushover's API returns an error code. """Exception which is raised when Pushover's API returns an error code.
The list of errors is stored in the :attr:`errors` attribute. The list of errors is stored in the :attr:`errors` attribute.
""" """
def __init__(self, errors): def __init__(self, errors):
Exception.__init__(self) Exception.__init__(self)
self.errors = errors self.errors = errors
def __str__(self): def __str__(self):
return "\n==> " + "\n==> ".join(self.errors) return "\n==> " + "\n==> ".join(self.errors)
class Request(object): class Request(object):
"""Base class to send a request to the Pushover server and check the return """Base class to send a request to the Pushover server and check the return
status code. The request is sent on instantiation and raises status code. The request is sent on instantiation and raises
a :class:`RequestError` exception when the request is rejected. a :class:`RequestError` exception when the request is rejected.
""" """
def __init__(self, method, url, payload): def __init__(self, method, url, payload):
files = {} files = {}
if "attachment" in payload: if "attachment" in payload:
files["attachment"] = payload["attachment"] files["attachment"] = payload["attachment"]
del payload["attachment"] del payload["attachment"]
self.payload = payload self.payload = payload
self.files = files self.files = files
request = getattr(requests, method)(url, params=payload, files=files) request = getattr(requests, method)(url, params=payload, files=files)
self.answer = request.json() self.answer = request.json()
if 400 <= request.status_code < 500: if 400 <= request.status_code < 500:
raise RequestError(self.answer["errors"]) raise RequestError(self.answer["errors"])
def __str__(self): def __str__(self):
return str(self.answer) return str(self.answer)
class MessageRequest(Request): class MessageRequest(Request):
"""This class represents a message request to the Pushover API. You do not """This class represents a message request to the Pushover API. You do not
need to create it yourself, but the :func:`Pushover.message` function need to create it yourself, but the :func:`Pushover.message` function
returns :class:`MessageRequest` objects. returns :class:`MessageRequest` objects.
The :attr:`answer` attribute contains a JSON representation of the answer 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 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. can poll the status of the notification with the :func:`poll` function.
""" """
params = { params = {
"expired": "expires_at", "expired": "expires_at",
"called_back": "called_back_at", "called_back": "called_back_at",
"acknowledged": "acknowledged_at", "acknowledged": "acknowledged_at",
} }
def __init__(self, payload): def __init__(self, payload):
Request.__init__(self, "post", MESSAGE_URL, payload) Request.__init__(self, "post", MESSAGE_URL, payload)
self.status = {"done": True} self.status = {"done": True}
if payload.get("priority", 0) == 2: if payload.get("priority", 0) == 2:
self.url = RECEIPT_URL + self.answer["receipt"] self.url = RECEIPT_URL + self.answer["receipt"]
self.status["done"] = False self.status["done"] = False
for param, when in MessageRequest.params.items(): for param, when in MessageRequest.params.items():
self.status[param] = False self.status[param] = False
self.status[when] = 0 self.status[when] = 0
def poll(self): def poll(self):
"""If the message request has a priority of 2, Pushover keeps sending """If the message request has a priority of 2, Pushover keeps sending
the same notification until the client acknowledges it. Calling the the same notification until the client acknowledges it. Calling the
:func:`poll` function fetches the status of the :class:`MessageRequest` :func:`poll` function fetches the status of the :class:`MessageRequest`
object until the notifications either expires, is acknowledged by the object until the notifications either expires, is acknowledged by the
client, or the callback url is reached. The status is available in the client, or the callback url is reached. The status is available in the
``status`` dictionary. ``status`` dictionary.
Returns ``True`` when the request has expired or been acknowledged and Returns ``True`` when the request has expired or been acknowledged and
``False`` otherwise so that a typical handling of a priority-2 ``False`` otherwise so that a typical handling of a priority-2
notification can look like this:: notification can look like this::
request = p.message("Urgent!", priority=2, expire=120, retry=60) request = p.message("Urgent!", priority=2, expire=120, retry=60)
while not request.poll(): while not request.poll():
# do something # do something
time.sleep(5) time.sleep(5)
print request.status print request.status
""" """
if not self.status["done"]: if not self.status["done"]:
r = Request("get", self.url + ".json", {"token": self.payload["token"]}) r = Request("get", self.url + ".json", {"token": self.payload["token"]})
for param, when in MessageRequest.params.items(): for param, when in MessageRequest.params.items():
self.status[param] = bool(r.answer[param]) self.status[param] = bool(r.answer[param])
self.status[when] = int(r.answer[when]) self.status[when] = int(r.answer[when])
for param in ["acknowledged_by", "acknowledged_by_device"]: for param in ["acknowledged_by", "acknowledged_by_device"]:
self.status[param] = r.answer[param] self.status[param] = r.answer[param]
self.status["last_delivered_at"] = int(r.answer["last_delivered_at"]) self.status["last_delivered_at"] = int(r.answer["last_delivered_at"])
if any(self.status[param] for param in MessageRequest.params): if any(self.status[param] for param in MessageRequest.params):
self.status["done"] = True self.status["done"] = True
return self.status["done"] return self.status["done"]
def cancel(self): def cancel(self):
"""If the message request has a priority of 2, Pushover keeps sending """If the message request has a priority of 2, Pushover keeps sending
the same notification until it either reaches its ``expire`` value or the same notification until it either reaches its ``expire`` value or
is aknowledged by the client. Calling the :func:`cancel` function is aknowledged by the client. Calling the :func:`cancel` function
cancels the notification early. cancels the notification early.
""" """
if not self.status["done"]: if not self.status["done"]:
return Request( return Request(
"post", self.url + "/cancel.json", {"token": self.payload["token"]} "post", self.url + "/cancel.json", {"token": self.payload["token"]}
) )
else: else:
return None return None
class Pushover(object): class Pushover(object):
"""This is the main class of the module. It represents a Pushover app and """This is the main class of the module. It represents a Pushover app and
is tied to a unique API token. is tied to a unique API token.
* ``token``: Pushover API token * ``token``: Pushover API token
""" """
_SOUNDS = None _SOUNDS = None
message_keywords = [ message_keywords = [
"title", "title",
"priority", "priority",
"sound", "sound",
"callback", "callback",
"timestamp", "timestamp",
"url", "url",
"url_title", "url_title",
"device", "device",
"retry", "retry",
"expire", "expire",
"html", "html",
"attachment", "attachment",
] ]
glance_keywords = ["title", "text", "subtext", "count", "percent", "device"] glance_keywords = ["title", "text", "subtext", "count", "percent", "device"]
def __init__(self, token): def __init__(self, token):
self.token = token self.token = token
@property @property
def sounds(self): def sounds(self):
"""Return a dictionary of sounds recognized by Pushover and that can be """Return a dictionary of sounds recognized by Pushover and that can be
used in a notification message. used in a notification message.
""" """
if not Pushover._SOUNDS: if not Pushover._SOUNDS:
request = Request("get", SOUND_URL, {"token": self.token}) request = Request("get", SOUND_URL, {"token": self.token})
Pushover._SOUNDS = request.answer["sounds"] Pushover._SOUNDS = request.answer["sounds"]
return Pushover._SOUNDS return Pushover._SOUNDS
def verify(self, user, device=None): def verify(self, user, device=None):
"""Verify that the `user` and optional `device` exist. Returns """Verify that the `user` and optional `device` exist. Returns
`None` when the user/device does not exist or a list of the user's `None` when the user/device does not exist or a list of the user's
devices otherwise. devices otherwise.
""" """
payload = {"user": user, "token": self.token} payload = {"user": user, "token": self.token}
if device: if device:
payload["device"] = device payload["device"] = device
try: try:
request = Request("post", USER_URL, payload) request = Request("post", USER_URL, payload)
except RequestError: except RequestError:
return None return None
else: else:
return request.answer["devices"] return request.answer["devices"]
def message(self, user, message, **kwargs): def message(self, user, message, **kwargs):
"""Send `message` to the user specified by `user`. It is possible """Send `message` to the user specified by `user`. It is possible
to specify additional properties of the message by passing keyword to specify additional properties of the message by passing keyword
arguments. The list of valid keywords is ``title, priority, sound, arguments. The list of valid keywords is ``title, priority, sound,
callback, timestamp, url, url_title, device, retry, expire and html`` callback, timestamp, url, url_title, device, retry, expire and html``
which are described in the Pushover API documentation. which are described in the Pushover API documentation.
For convenience, you can simply set ``timestamp=True`` to set the For convenience, you can simply set ``timestamp=True`` to set the
timestamp to the current timestamp. timestamp to the current timestamp.
An image can be attached to a message by passing a file-like object An image can be attached to a message by passing a file-like object
to the `attachment` keyword argument. to the `attachment` keyword argument.
This method returns a :class:`MessageRequest` object. This method returns a :class:`MessageRequest` object.
""" """
payload = {"message": message, "user": user, "token": self.token} payload = {"message": message, "user": user, "token": self.token}
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in Pushover.message_keywords: if key not in Pushover.message_keywords:
raise ValueError("{0}: invalid message parameter".format(key)) raise ValueError("{0}: invalid message parameter".format(key))
elif key == "timestamp" and value is True: elif key == "timestamp" and value is True:
payload[key] = int(time.time()) payload[key] = int(time.time())
elif key == "sound" and value not in self.sounds: elif key == "sound" and value not in self.sounds:
raise ValueError("{0}: invalid sound".format(value)) raise ValueError("{0}: invalid sound".format(value))
else: else:
payload[key] = value payload[key] = value
return MessageRequest(payload) return MessageRequest(payload)
def glance(self, user, **kwargs): def glance(self, user, **kwargs):
"""Send a glance to the user. The default property is ``text``, as this """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 is used on most glances, however a valid glance does not need to
require text and can be constructed using any combination of valid require text and can be constructed using any combination of valid
keyword properties. The list of valid keywords is ``title, text, keyword properties. The list of valid keywords is ``title, text,
subtext, count, percent and device`` which are described in the subtext, count, percent and device`` which are described in the
Pushover Glance API documentation. Pushover Glance API documentation.
This method returns a :class:`GlanceRequest` object. This method returns a :class:`GlanceRequest` object.
""" """
payload = {"user": user, "token": self.token} payload = {"user": user, "token": self.token}
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in Pushover.glance_keywords: if key not in Pushover.glance_keywords:
raise ValueError("{0}: invalid glance parameter".format(key)) raise ValueError("{0}: invalid glance parameter".format(key))
else: else:
payload[key] = value payload[key] = value
return Request("post", GLANCE_URL, payload) return Request("post", GLANCE_URL, payload)

View File

@ -9,35 +9,45 @@ def build_tor_list(self):
torrent = self.torrent_list.pop() torrent = self.torrent_list.pop()
if self.use_log: if self.use_log:
self.tl.debug(f'---Analyzing ["{torrent["name"][0:20]}..."] {torrent["infohash_v1"]}---') self.tl.debug(f'---Analyzing ["{torrent["name"][0:20]}..."] {torrent["infohash_v1"]}---')
if is_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()): # Need a way to tag when the tracker is blank
if is_tag_blank(torrent['tags']):
self.qbt_client.torrents_add_tags(self.tracker_protected_tag,torrent['hash'])
if self.use_log:
self.tl.debug(f'Tagging Protected torrent: ["{torrent["name"][0:20]}..."] {torrent["tracker"]}hash: {torrent["hash"]}')
self.tracker_list.append(torrent)
if is_not_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
if is_tag_blank(torrent['tags']):
self.qbt_client.torrents_add_tags(self.tracker_non_protected_tag,torrent['hash'])
if self.use_log:
self.tl.debug(f'Tagging Non-protected torrent: ["{torrent["name"][0:20]}..."] {torrent["tracker"]}hash: {torrent["hash"]}')
self.tracker_list.append(torrent)
if is_ignored_tag(self.ignored_tags.values(),torrent['tags']):
self.ignored_counter += 1
self.tl.info(f'Ignored tag: ["{torrent["name"][0:20]}..."] tags: {torrent["tags"]} hash: {torrent["hash"]}')
continue
if is_preme(torrent['added_on'], self.minimum_age, self.t.time()):
self.preme_tor_counter += 1
self.tl.debug(f'Premature torrent: ["{torrent["name"][0:20]}..."] hash: {torrent["hash"]}')
continue
if is_cat_ignored(torrent['category'], self.cat_whitelist.values()):
self.tl.info(f'Ignored category: ["{torrent["name"][0:20]}..."] category:[{torrent["category"]}] hash: {torrent["hash"]}')
self.ignored_counter += 1
continue
if is_tracker_blank(torrent['tracker']): if is_tracker_blank(torrent['tracker']):
if self.use_log: if self.use_log:
self.tl.warning(f'Torrent doesn\'t have a tracker ["{torrent["name"][0:20]}..."] [{torrent["tracker"]}]hash: {torrent["hash"]}') self.tl.warning(f'Torrent doesn\'t have a tracker ["{torrent["name"][0:20]}..."] [{torrent["tracker"]}]hash: {torrent["hash"]}')
self.ignored_counter += 1 self.ignored_counter += 1
continue continue
elif is_cat_ignored(torrent['category'], self.cat_whitelist.values()):
if self.use_log:
self.tl.info(f'Ignored category: ["{torrent["name"][0:20]}..."] category:[{torrent["category"]}] hash: {torrent["hash"]}')
self.ignored_counter += 1
continue
elif is_ignored_tag(self.ignored_tags.values(),torrent['tags']):
if self.use_log:
self.tl.info(f'Ignored tag: ["{torrent["name"][0:20]}..."] tags: {torrent["tags"]} hash: {torrent["hash"]}')
self.ignored_counter += 1
continue
if is_tag_blank(torrent['tags']):
if self.use_log:
self.tl.debug(f'Tagging new torrent: ["{torrent["name"][0:20]}..."] {torrent["tracker"]}hash: {torrent["hash"]}')
if is_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
self.qbt_client.torrents_add_tags(self.tracker_protected_tag,torrent['hash'])
elif is_not_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
self.qbt_client.torrents_add_tags(self.tracker_non_protected_tag,torrent['hash'])
if is_preme(torrent['added_on'], self.minimum_age, self.t.time()):
self.preme_tor_counter += 1
self.tl.debug(f'Premature torrent: ["{torrent["name"][0:20]}..."] hash: {torrent["hash"]}')
continue
elif is_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
if is_tag_blank(torrent['tags']):
self.qbt_client.torrents_add_tags(self.tracker_protected_tag,torrent['hash'])
if self.use_log:
self.tl.debug(f'Tagging Protected torrent: ["{torrent["name"][0:20]}..."] {torrent["tracker"]}hash: {torrent["hash"]}')
self.tracker_list.append(torrent)
elif is_not_protected_tracker(torrent['tracker'], self.tracker_whitelist.values()):
if is_tag_blank(torrent['tags']):
self.qbt_client.torrents_add_tags(self.tracker_non_protected_tag,torrent['hash'])
if self.use_log:
self.tl.debug(f'Tagging Non-protected torrent: ["{torrent["name"][0:20]}..."] {torrent["tracker"]}hash: {torrent["hash"]}')
self.tracker_list.append(torrent)
def is_preme(added, minage, time): def is_preme(added, minage, time):

View File

@ -90,10 +90,8 @@ class TestQbitmaid(unittest.TestCase):
def test_isignoredtag_sanity(self): def test_isignoredtag_sanity(self):
self.assertTrue(is_ignored_tag(['a','b','c'], 'first,second,third,a')) self.assertTrue(is_ignored_tag(['a','b','c'], 'first,second,third,a'))
def test_isignoredtag_sanity(self): def test_isignoredtag(self):
self.assertTrue(is_ignored_tag(['a','b','c'], 'first,second,third,a')) self.assertTrue(is_ignored_tag(['save'], 'save,public,ipt'))
# def test__sanity(self): # def test__sanity(self):
# pass # pass