Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get icon_data. #1090

Closed
dharmx opened this issue Jul 20, 2022 · 9 comments
Closed

Get icon_data. #1090

dharmx opened this issue Jul 20, 2022 · 9 comments

Comments

@dharmx
Copy link

dharmx commented Jul 20, 2022

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
# Dunstrc here
# Not applicable.

Please, ask if you need any additional info.

@dharmx
Copy link
Author

dharmx commented Jul 21, 2022

#!/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:

same-image.mp4

@dharmx
Copy link
Author

dharmx commented Jul 22, 2022

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
Copy link
Author

dharmx commented Jul 22, 2022

Closing!

@dharmx dharmx closed this as completed Jul 22, 2022
@fwsmit
Copy link
Member

fwsmit commented Jul 22, 2022

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

@scarlion1
Copy link

@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
Copy link
Author

dharmx commented Sep 10, 2023

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
Copy link

@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
Copy link
Author

dharmx commented Sep 16, 2023

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.

output2.mp4

@scarlion1
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants