mirror of
https://github.com/morpheus65535/bazarr.git
synced 2026-07-31 10:07:37 -04:00
451 lines
14 KiB
Python
451 lines
14 KiB
Python
# BSD 2-Clause License
|
|
#
|
|
# Apprise - Push Notification Library.
|
|
# Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# 1. Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions and the following disclaimer.
|
|
#
|
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import sys
|
|
|
|
from ..common import NotifyImageSize, NotifyType
|
|
from ..locale import gettext_lazy as _
|
|
from ..utils.parse import parse_bool
|
|
from .base import NotifyBase
|
|
|
|
# Default our global support flag
|
|
NOTIFY_DBUS_SUPPORT_ENABLED = False
|
|
|
|
# Image support is dependant on the GdkPixbuf library being available
|
|
NOTIFY_DBUS_IMAGE_SUPPORT = False
|
|
|
|
# Initialize our mainloops
|
|
LOOP_GLIB = None
|
|
LOOP_QT = None
|
|
|
|
|
|
try:
|
|
# D-Bus Message Bus Daemon 1.12.XX Essentials
|
|
from dbus import Byte, ByteArray, DBusException, Interface, SessionBus
|
|
|
|
#
|
|
# now we try to determine which mainloop(s) we can access
|
|
#
|
|
|
|
# glib/dbus
|
|
try:
|
|
from dbus.mainloop.glib import DBusGMainLoop
|
|
|
|
LOOP_GLIB = DBusGMainLoop()
|
|
|
|
except ImportError: # pragma: no cover
|
|
# No problem
|
|
pass
|
|
|
|
# qt
|
|
try:
|
|
from dbus.mainloop.qt import DBusQtMainLoop
|
|
|
|
LOOP_QT = DBusQtMainLoop(set_as_default=True)
|
|
|
|
except ImportError:
|
|
# No problem
|
|
pass
|
|
|
|
# We're good as long as at least one
|
|
NOTIFY_DBUS_SUPPORT_ENABLED = LOOP_GLIB is not None or LOOP_QT is not None
|
|
|
|
# ImportError: When using gi.repository you must not import static modules
|
|
# like "gobject". Please change all occurrences of "import gobject" to
|
|
# "from gi.repository import GObject".
|
|
# See: https://bugzilla.gnome.org/show_bug.cgi?id=709183
|
|
if "gobject" in sys.modules: # pragma: no cover
|
|
del sys.modules["gobject"]
|
|
|
|
try:
|
|
# The following is required for Image/Icon loading only
|
|
import gi
|
|
|
|
gi.require_version("GdkPixbuf", "2.0")
|
|
from gi.repository import GdkPixbuf
|
|
|
|
NOTIFY_DBUS_IMAGE_SUPPORT = True
|
|
|
|
except (ImportError, ValueError, AttributeError):
|
|
# No problem; this will get caught in outer try/catch
|
|
|
|
# A ValueError will get thrown upon calling gi.require_version() if
|
|
# GDK/GTK isn't installed on the system but gi is.
|
|
pass
|
|
|
|
except ImportError:
|
|
# No problem; we just simply can't support this plugin; we could
|
|
# be in microsoft windows, or we just don't have the python-gobject
|
|
# library available to us (or maybe one we don't support)?
|
|
pass
|
|
|
|
# Define our supported protocols and the loop to assign them.
|
|
# The key to value pairs are the actual supported schema's matched
|
|
# up with the Main Loop they should reference when accessed.
|
|
MAINLOOP_MAP = {
|
|
"qt": LOOP_QT,
|
|
"kde": LOOP_QT,
|
|
"dbus": LOOP_QT if LOOP_QT else LOOP_GLIB,
|
|
}
|
|
|
|
|
|
# Urgencies
|
|
class DBusUrgency:
|
|
LOW = 0
|
|
NORMAL = 1
|
|
HIGH = 2
|
|
|
|
|
|
DBUS_URGENCIES = {
|
|
# Note: This also acts as a reverse lookup mapping
|
|
DBusUrgency.LOW: "low",
|
|
DBusUrgency.NORMAL: "normal",
|
|
DBusUrgency.HIGH: "high",
|
|
}
|
|
|
|
DBUS_URGENCY_MAP = {
|
|
# Maps against string 'low'
|
|
"l": DBusUrgency.LOW,
|
|
# Maps against string 'moderate'
|
|
"m": DBusUrgency.LOW,
|
|
# Maps against string 'normal'
|
|
"n": DBusUrgency.NORMAL,
|
|
# Maps against string 'high'
|
|
"h": DBusUrgency.HIGH,
|
|
# Maps against string 'emergency'
|
|
"e": DBusUrgency.HIGH,
|
|
# Entries to additionally support (so more like DBus's API)
|
|
"0": DBusUrgency.LOW,
|
|
"1": DBusUrgency.NORMAL,
|
|
"2": DBusUrgency.HIGH,
|
|
}
|
|
|
|
|
|
class NotifyDBus(NotifyBase):
|
|
"""A wrapper for local DBus/Qt Notifications."""
|
|
|
|
# Set our global enabled flag
|
|
enabled = NOTIFY_DBUS_SUPPORT_ENABLED
|
|
|
|
requirements = {
|
|
# Define our required packaging in order to work
|
|
"details": _("libdbus-1.so.x must be installed.")
|
|
}
|
|
|
|
# The default descriptive name associated with the Notification
|
|
service_name = _("DBus Notification")
|
|
|
|
# The services URL
|
|
service_url = "http://www.freedesktop.org/Software/dbus/"
|
|
|
|
# The default protocols
|
|
# Python 3 keys() does not return a list object, it is its own dict_keys()
|
|
# object if we were to reference, we wouldn't be backwards compatible with
|
|
# Python v2. So converting the result set back into a list makes us
|
|
# compatible
|
|
protocol = list(MAINLOOP_MAP.keys())
|
|
|
|
# A URL that takes you to the setup/help of the specific protocol
|
|
setup_url = "https://appriseit.com/services/dbus/"
|
|
|
|
# No throttling required for DBus queries
|
|
request_rate_per_sec = 0
|
|
|
|
# Allows the user to specify the NotifyImageSize object
|
|
image_size = NotifyImageSize.XY_128
|
|
|
|
# The number of milliseconds to keep the message present for
|
|
message_timeout_ms = 13000
|
|
|
|
# Limit results to just the first 10 line otherwise there is just to much
|
|
# content to display
|
|
body_max_line_count = 10
|
|
|
|
# The following are required to hook into the notifications:
|
|
dbus_interface = "org.freedesktop.Notifications"
|
|
dbus_setting_location = "/org/freedesktop/Notifications"
|
|
|
|
# No URL Identifier will be defined for this service as there simply isn't
|
|
# enough details to uniquely identify one dbus:// from another.
|
|
url_identifier = False
|
|
|
|
# Define object templates
|
|
templates = ("{schema}://",)
|
|
|
|
# Define our template arguments
|
|
template_args = dict(
|
|
NotifyBase.template_args,
|
|
**{
|
|
"urgency": {
|
|
"name": _("Urgency"),
|
|
"type": "choice:int",
|
|
"values": DBUS_URGENCIES,
|
|
"default": DBusUrgency.NORMAL,
|
|
},
|
|
"priority": {
|
|
# Apprise uses 'priority' everywhere; it's just a nice
|
|
# consistent feel to be able to use it here as well. Just map
|
|
# the value back to 'priority'
|
|
"alias_of": "urgency",
|
|
},
|
|
"x": {
|
|
"name": _("X-Axis"),
|
|
"type": "int",
|
|
"min": 0,
|
|
"map_to": "x_axis",
|
|
},
|
|
"y": {
|
|
"name": _("Y-Axis"),
|
|
"type": "int",
|
|
"min": 0,
|
|
"map_to": "y_axis",
|
|
},
|
|
"image": {
|
|
"name": _("Include Image"),
|
|
"type": "bool",
|
|
"default": True,
|
|
"map_to": "include_image",
|
|
},
|
|
},
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
urgency=None,
|
|
x_axis=None,
|
|
y_axis=None,
|
|
include_image=True,
|
|
**kwargs,
|
|
):
|
|
"""Initialize DBus Object."""
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
# Track our notifications
|
|
self.registry = {}
|
|
|
|
# Store our schema; default to dbus
|
|
self.schema = kwargs.get("schema", "dbus")
|
|
|
|
if self.schema not in MAINLOOP_MAP:
|
|
msg = f"The schema specified ({self.schema}) is not supported."
|
|
self.logger.warning(msg)
|
|
raise TypeError(msg)
|
|
|
|
# The urgency of the message
|
|
self.urgency = int(
|
|
NotifyDBus.template_args["urgency"]["default"]
|
|
if urgency is None
|
|
else next(
|
|
(
|
|
v
|
|
for k, v in DBUS_URGENCY_MAP.items()
|
|
if str(urgency).lower().startswith(k)
|
|
),
|
|
NotifyDBus.template_args["urgency"]["default"],
|
|
)
|
|
)
|
|
|
|
# Our x/y axis settings
|
|
if x_axis or y_axis:
|
|
try:
|
|
self.x_axis = int(x_axis)
|
|
self.y_axis = int(y_axis)
|
|
|
|
except (TypeError, ValueError):
|
|
# Invalid x/y values specified
|
|
msg = (
|
|
f"The x,y coordinates specified ({x_axis},{y_axis}) are"
|
|
" invalid."
|
|
)
|
|
self.logger.warning(msg)
|
|
raise TypeError(msg) from None
|
|
else:
|
|
self.x_axis = None
|
|
self.y_axis = None
|
|
|
|
# Track whether we want to add an image to the notification.
|
|
self.include_image = include_image
|
|
|
|
def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
|
|
"""Perform DBus Notification."""
|
|
# Acquire our session
|
|
try:
|
|
session = SessionBus(mainloop=MAINLOOP_MAP[self.schema])
|
|
|
|
except DBusException as e:
|
|
# Handle exception
|
|
self.logger.warning("Failed to send DBus notification.")
|
|
self.logger.debug(f"DBus Exception: {e}")
|
|
return False
|
|
|
|
# If there is no title, but there is a body, swap the two to get rid
|
|
# of the weird whitespace
|
|
if not title:
|
|
title = body
|
|
body = ""
|
|
|
|
# acquire our dbus object
|
|
dbus_obj = session.get_object(
|
|
self.dbus_interface,
|
|
self.dbus_setting_location,
|
|
)
|
|
|
|
# Acquire our dbus interface
|
|
dbus_iface = Interface(
|
|
dbus_obj,
|
|
dbus_interface=self.dbus_interface,
|
|
)
|
|
|
|
# image path
|
|
icon_path = (
|
|
None
|
|
if not self.include_image
|
|
else self.image_path(notify_type, extension=".ico")
|
|
)
|
|
|
|
# Our meta payload
|
|
meta_payload = {"urgency": Byte(self.urgency)}
|
|
|
|
if not (self.x_axis is None and self.y_axis is None):
|
|
# Set x/y access if these were set
|
|
meta_payload["x"] = self.x_axis
|
|
meta_payload["y"] = self.y_axis
|
|
|
|
if NOTIFY_DBUS_IMAGE_SUPPORT and icon_path:
|
|
try:
|
|
# Use Pixbuf to create the proper image type
|
|
image = GdkPixbuf.Pixbuf.new_from_file(icon_path)
|
|
|
|
# Associate our image to our notification
|
|
meta_payload["icon_data"] = (
|
|
image.get_width(),
|
|
image.get_height(),
|
|
image.get_rowstride(),
|
|
image.get_has_alpha(),
|
|
image.get_bits_per_sample(),
|
|
image.get_n_channels(),
|
|
ByteArray(image.get_pixels()),
|
|
)
|
|
|
|
except Exception as e:
|
|
self.logger.warning(
|
|
"Could not load notification icon (%s).", icon_path
|
|
)
|
|
self.logger.debug(f"DBus Exception: {e}")
|
|
|
|
try:
|
|
# Always call throttle() before any remote execution is made
|
|
self.throttle()
|
|
|
|
dbus_iface.Notify(
|
|
# Application Identifier
|
|
self.app_id,
|
|
# Message ID (0 = New Message)
|
|
0,
|
|
# Icon (str) - not used
|
|
"",
|
|
# Title
|
|
str(title),
|
|
# Body
|
|
str(body),
|
|
# Actions
|
|
[],
|
|
# Meta
|
|
meta_payload,
|
|
# Message Timeout
|
|
self.message_timeout_ms,
|
|
)
|
|
|
|
self.logger.info("Sent DBus notification.")
|
|
|
|
except Exception as e:
|
|
self.logger.warning("Failed to send DBus notification.")
|
|
self.logger.debug(f"DBus Exception: {e}")
|
|
return False
|
|
|
|
return True
|
|
|
|
def url(self, privacy=False, *args, **kwargs):
|
|
"""Returns the URL built dynamically based on specified arguments."""
|
|
|
|
# Define any URL parameters
|
|
params = {
|
|
"image": "yes" if self.include_image else "no",
|
|
"urgency": (
|
|
DBUS_URGENCIES[self.template_args["urgency"]["default"]]
|
|
if self.urgency not in DBUS_URGENCIES
|
|
else DBUS_URGENCIES[self.urgency]
|
|
),
|
|
}
|
|
|
|
# Extend our parameters
|
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
|
|
|
# x in (x,y) screen coordinates
|
|
if self.x_axis:
|
|
params["x"] = str(self.x_axis)
|
|
|
|
# y in (x,y) screen coordinates
|
|
if self.y_axis:
|
|
params["y"] = str(self.y_axis)
|
|
|
|
return f"{self.schema}://_/?{NotifyDBus.urlencode(params)}"
|
|
|
|
@staticmethod
|
|
def parse_url(url):
|
|
"""There are no parameters nessisary for this protocol; simply having
|
|
gnome:// is all you need.
|
|
|
|
This function just makes sure that is in place.
|
|
"""
|
|
|
|
results = NotifyBase.parse_url(url, verify_host=False)
|
|
|
|
# Include images with our message
|
|
results["include_image"] = parse_bool(
|
|
results["qsd"].get("image", True)
|
|
)
|
|
|
|
# DBus supports urgency, but we we also support the keyword priority
|
|
# so that it is consistent with some of the other plugins
|
|
if "priority" in results["qsd"] and len(results["qsd"]["priority"]):
|
|
# We intentionally store the priority in the urgency section
|
|
results["urgency"] = NotifyDBus.unquote(results["qsd"]["priority"])
|
|
|
|
if "urgency" in results["qsd"] and len(results["qsd"]["urgency"]):
|
|
results["urgency"] = NotifyDBus.unquote(results["qsd"]["urgency"])
|
|
|
|
# handle x,y coordinates
|
|
if "x" in results["qsd"] and len(results["qsd"]["x"]):
|
|
results["x_axis"] = NotifyDBus.unquote(results["qsd"].get("x"))
|
|
|
|
if "y" in results["qsd"] and len(results["qsd"]["y"]):
|
|
results["y_axis"] = NotifyDBus.unquote(results["qsd"].get("y"))
|
|
|
|
return results
|