dunst-project / dunst

Lightweight and customizable notification daemon
https://dunst-project.org
Other
4.61k stars 342 forks source link

Get icon_data. #1090

Closed dharmx closed 2 years ago

dharmx commented 2 years ago

Issue description

How do I get the icon_data through a script. For example, Discord sends the profile picture in byte format and Spotify sends the album art in byte format. I am making a notification manager so I was wondering if getting these hints would be possible or not and if so how? Also, the dunstctl history part also doesn't have them. The icon data path is empty!

However, upon monitoring the DBUS interface(?) I do see that the icon data is being returned. Note that the notifications itself work fine! The images do show when I am pinged from Discord. I just want the icon bytes so I can parse it in my script.

bytes

Installation info

Version: Dunst - A customizable and lightweight notification-daemon v1.8.1-60-g3bba0b0 Install type: From AUR from here Window manager: BSPWM

Minimal dunstrc ```ini # Dunstrc here # Not applicable. ```

Please, ask if you need any additional info.

dharmx commented 2 years ago
#!/usr/bin/env python

import contextlib
import datetime

import dbus
import gi

gi.require_version("GdkPixbuf", "2.0")
from gi.repository import GdkPixbuf, GLib
from dbus.mainloop.glib import DBusGMainLoop

def unwrap(value):
    # Try to trivially translate a dictionary's elements into nice string
    # formatting.
    if isinstance(value, dbus.ByteArray):
        return "".join([str(byte) for byte in value])
    if isinstance(value, (dbus.Array, list, tuple)):
        return [unwrap(item) for item in value]
    if isinstance(value, (dbus.Dictionary, dict)):
        return dict([(unwrap(x), unwrap(y)) for x, y in value.items()])
    if isinstance(value, (dbus.Signature, dbus.String)):
        return str(value)
    if isinstance(value, dbus.Boolean):
        return bool(value)
    if isinstance(
        value,
        (dbus.Int16, dbus.UInt16, dbus.Int32, dbus.UInt32, dbus.Int64, dbus.UInt64),
    ):
        return int(value)
    if isinstance(value, dbus.Byte):
        return bytes([int(value)])
    return value

def save_image_bytes(px_args):
    # gets image data and saves it to file
    save_path = f"/tmp/image-{datetime.datetime.now().strftime('%s')}.png"
    # https://specifications.freedesktop.org/notification-spec/latest/ar01s08.html
    # https://specifications.freedesktop.org/notification-spec/latest/ar01s05.html
    GdkPixbuf.Pixbuf.new_from_data(
        width=px_args[0],
        height=px_args[1],
        has_alpha=px_args[3],
        data=px_args[6],
        colorspace=GdkPixbuf.Colorspace.RGB,
        rowstride=px_args[2],
        bits_per_sample=px_args[4],
    ).savev(save_path, "png")
    print(len(px_args[6])) # DEBUG: The sizes are the same! Although, DUNST shows images changes.
    return save_path

def message_callback(_, message):
    if type(message) != dbus.lowlevel.MethodCallMessage:
        return
    args_list = message.get_args_list()
    args_list = [unwrap(item) for item in args_list]
    details = {
        "appname": args_list[0],
        "summary": args_list[3],
        "body": args_list[4],
        "urgency": args_list[6]["urgency"],
        "iconpath": None,
    }
    if args_list[2]:
        details["iconpath"] = args_list[2]
    with contextlib.suppress(KeyError):
        # for some reason args_list[6]["icon_data"][6] i.e. the byte data
        # does not change unless I restart spotify but, the song title
        # (body / summary) change gets picked up.
        details["iconpath"] = save_image_bytes(args_list[6]["icon_data"])
    print(details) # DEBUG 

DBusGMainLoop(set_as_default=True)

rules = {
    "interface": "org.freedesktop.Notifications",
    "member": "Notify",
    "eavesdrop": "true", # https://bugs.freedesktop.org/show_bug.cgi?id=39450
}

bus = dbus.SessionBus() # TODO: Use proxy.
# is there a better way of doing this? Like a adding some sort of signal?
# I could not figure out the bus.add_signal_receiver thing. How do I use this?
# Is this needed at all?
bus.add_match_string(",".join([f"{key}={value}" for key, value in rules.items()]))
bus.add_message_filter(message_callback)

loop = GLib.MainLoop()
try:
    loop.run()
except KeyboardInterrupt:
    loop.quit()
    bus.close()

# vim:filetype=python

Cooked this but the image_data never changes any idea why? Demo:

https://user-images.githubusercontent.com/80379926/180319350-9e129bb2-d02e-4d08-a340-812e5188c92c.mp4

dharmx commented 2 years ago

I cannot believe I am so stupid. How am I even alive. In the above script I have verified that icon_data is deprecated on favor of image-data. So, I just needed to change that. Additionally, I have also reviewed this issue #804 and decided it would be a headache to maintain this.

Following is the corrected script if someone needs it!

#!/usr/bin/env python

import contextlib
import datetime

import dbus
import gi

gi.require_version("GdkPixbuf", "2.0")
from gi.repository import GdkPixbuf, GLib
from dbus.mainloop.glib import DBusGMainLoop

def unwrap(value):
    # Try to trivially translate a dictionary's elements into nice string
    # formatting.
    if isinstance(value, dbus.ByteArray):
        return "".join([str(byte) for byte in value])
    if isinstance(value, (dbus.Array, list, tuple)):
        return [unwrap(item) for item in value]
    if isinstance(value, (dbus.Dictionary, dict)):
        return dict([(unwrap(x), unwrap(y)) for x, y in value.items()])
    if isinstance(value, (dbus.Signature, dbus.String)):
        return str(value)
    if isinstance(value, dbus.Boolean):
        return bool(value)
    if isinstance(
        value,
        (dbus.Int16, dbus.UInt16, dbus.Int32, dbus.UInt32, dbus.Int64, dbus.UInt64),
    ):
        return int(value)
    if isinstance(value, dbus.Byte):
        return bytes([int(value)])
    return value

def save_img_byte(px_args):
    # gets image data and saves it to file
    save_path = f"/tmp/image-{datetime.datetime.now().strftime('%s')}.png"
    # https://specifications.freedesktop.org/notification-spec/latest/ar01s08.html
    # https://specifications.freedesktop.org/notification-spec/latest/ar01s05.html
    GdkPixbuf.Pixbuf.new_from_bytes(
        width=px_args[0],
        height=px_args[1],
        has_alpha=px_args[3],
        data=GLib.Bytes(px_args[6]),
        colorspace=GdkPixbuf.Colorspace.RGB,
        rowstride=px_args[2],
        bits_per_sample=px_args[4],
    ).savev(save_path, "png")
    return save_path

def message_callback(_, message):
    if type(message) != dbus.lowlevel.MethodCallMessage:
        return
    args_list = message.get_args_list()
    args_list = [unwrap(item) for item in args_list]
    details = {
        "appname": args_list[0],
        "summary": args_list[3],
        "body": args_list[4],
        "urgency": args_list[6]["urgency"],
        "iconpath": None,
    }
    if args_list[2]:
        details["iconpath"] = args_list[2]
    with contextlib.suppress(KeyError):
        # for some reason args_list[6]["icon_data"][6] i.e. the byte data
        # does not change unless I restart spotify but, the song title
        # (body / summary) change gets picked up.
        details["iconpath"] = save_img_byte(args_list[6]["image-data"])
    print(details) # DEBUG

DBusGMainLoop(set_as_default=True)

rules = {
    "interface": "org.freedesktop.Notifications",
    "member": "Notify",
    "eavesdrop": "true", # https://bugs.freedesktop.org/show_bug.cgi?id=39450
}

bus = dbus.SessionBus()
bus.add_match_string(",".join([f"{key}={value}" for key, value in rules.items()]))
bus.add_message_filter(message_callback)

loop = GLib.MainLoop()
try:
    loop.run()
except KeyboardInterrupt:
    bus.close()

# vim:filetype=python
dharmx commented 2 years ago

Closing!

fwsmit commented 2 years ago

Great that you've solved it and thanks for providing your solution!

scarlion1 commented 1 year ago

@dharmx I cannot believe you are so smart!  So if I run this script while I receive notifs then the image-data goes into /tmp?  What if I had dbus-monitor "interface='org.freedesktop.Notifications', member='Notify'" running previously and I have some raw data available such as:

dict entry(
   string "image-data"
   variant             struct {
         int32 58
         int32 58
         int32 232
         boolean true
         int32 8
         int32 4
         array of bytes [
            00 00 00 00 00 00 00 07 00 00 00 3e 00 00 00 76 02 02 02 9d
            01 01 01 c4 00 00 00 df 00 00 00 ed 00 00 00 fa 00 00 00 fa
            00 00 00 ed 00 00 00 df 00 00 00 c4 00 00 00 9d 00 00 00 76
            [...]
            00 00 00 9c 00 00 00 c3 01 00 00 df 01 00 01 ec 00 01 00 f9
            00 01 00 f9 00 01 00 ec 02 00 01 df 01 01 01 c3 00 00 02 9c
            00 00 02 75 00 00 00 3d 00 00 00 07 00 00 00 00 00 00 00 00
            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
         ]
      }
)

Could your script be used to parse the image-data and decode/save?  If so, how?  or, you think there's a better way to decode/save the image-data from this dbus-monitor output?  thx!

dharmx commented 1 year ago

Could your script be used to parse the image-data and decode/save?

The script does exactly that. You Just execute that and forget. It'll save the images to /tmp/ and prints a JSON metadata of that song. If you want to know when the value is sent then you'd read using a while loop. Note that this script depends on Gio bindings for python.

./notify | while read -r metadata; do
  # read using jq or whatever
done
scarlion1 commented 1 year ago

@dharmx I can't figure it out, for sure I'm the stupid one here. 😁  I do have the dbus-monitor command running as a user systemd service, so when I get a notif, the raw data gets saved in the system journal and I can retrieve it with journalctl.  I tried launching your script and waited for the next notif to come in, plus several more, but it's not picking up the data and nothing is saved to /tmp.  I also tried copying the data for a raw notif from the journal and tried cat notif.txt | yourScript.py but that doesn't do anything either, it just seems to hang actually.  Running on Debian here and python3-gi is installed.  Any idea what's wrong?  I guess it's me tbh, do I need to be running Dunst for this to work? 😅  I just want to decode the image-data somehow back into an image and this thread came up in a search...

dharmx commented 1 year ago

do I need to be running Dunst for this to work

Any, notification daemon should work. Like dunst, tiramisu, naughty (AwesomeWM), etc. But yes, you would need to have any one of these installed.

Here's a demo and code.

https://github.com/dunst-project/dunst/assets/80379926/d835f1ad-ec8d-4646-a4a3-813ba1e88f51

scarlion1 commented 1 year ago

I mean I just have stock Gnome with default FreeDesktop notifications I guess.  /usr/bin/dbus-monitor "interface='org.freedesktop.Notifications'" is running as a systemd service, so in addition to seeing the actual notif, the raw data is saved in the journal.