dasJ/spotifywm

No longer working with Spotify 1.1.55

Closed this issue ยท 28 comments

After updating Spotify to latest 1.1.55.498.gf9a83c60, spotifywm is no longer working.

Output looks OK:

[spotifywm] attached to spotify
[spotifywm] attached to spotify
[spotifywm] attached to spotify

[spotifywm] attached to spotify
[spotifywm] spotify window found

But the window manager no longer applies any settings to the Spotify window on startup.

This same setup was working on previous versions of spotify-client, with nothing else changed.

Experiencing the same problem. I installed today bspwm and spotify doesn't follow the rules that I put in the bspwmrc.
Same output for me.
[spotifywm] attached to spotify
[spotifywm] attached to spotify

[spotifywm] attached to spotify
[spotifywm] spotify window found

In the bspwmrc I put:
bspc rule -a Spotify desktop='^3'

Same stuff on DWM

same on xmonad

dasJ commented

Same on sway

I'm not too terribly familiar with how X works but I compiled a shared library very similiar to spotifywm that intercepts all of the windows creation and mapping functions I could find:

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif /* _GNU_SOURCE */

#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include <assert.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

// Provided by glibc
extern char *program_invocation_short_name;

static void *dllXCreateWindow = NULL;
static void *dllXCreateSimpleWindow = NULL;
static void *dllXMapRaised = NULL;
static void *dllXMapSubwindows = NULL;
static void *dllXMapWindow = NULL;

static void SetClassHint(Display *display, Window w, char *name, char *cls)
{
    XClassHint *hint = XAllocClassHint();
    if (hint)
    {
        hint->res_name  = name;
        hint->res_class = cls;
        if (!XSetClassHint(display, w, hint))
        {
            fprintf(stderr, "[spotifywm] Failed to set class hint for window %ld\n", w);
        }

        XFree(hint);
    }
    else
    {
        fprintf(stderr, "[spotifywm] Failed to allocate class hints\n");
    }
}

Window XCreateWindow(Display *display, Window parent, int x, int y, unsigned int width, unsigned int height, unsigned int border_width, int depth, unsigned int class, Visual *visual, unsigned long valuemask, XSetWindowAttributes *attributes)
{
    Window w = ((int (*)(Display *, Window, int, int, unsigned int, unsigned int, unsigned int, int, unsigned int, Visual *, unsigned long, XSetWindowAttributes *)) dllXCreateWindow)(display, parent, x, y, width, height, border_width, depth, class, visual, valuemask, attributes);
    fprintf(stdout, "[spotifywm] 0x%lx = XCreateWindow(parent=0x%lx)\n", w, parent);
    return w;
}

Window XCreateSimpleWindow(Display *display, Window parent, int x, int y, unsigned int width, unsigned int height, unsigned int border_width, unsigned long border, unsigned long background)
{
    Window w = ((Window (*)(Display *, Window, int, int, unsigned int, unsigned int, unsigned int, unsigned long, unsigned long)) dllXCreateSimpleWindow)(display, parent, x, y, width, height, border_width, border, background);
    fprintf(stdout, "[spotifywm] 0x%lx = XCreateSimpleWindow(parent=0x%lx)\n", w, parent);
    return w;
}

int XMapRaised(Display *display, Window w)
{
    fprintf(stdout, "[spotifywm] XMapRaised(window=0x%lx)\n", w);
    return ((int (*)(Display *, Window)) dllXMapRaised)(display, w);
}

int XMapSubwindows(Display *display, Window w)
{
    fprintf(stdout, "[spotifywm] XMapSubwindows(window=0x%lx)\n", w);
    return ((int (*)(Display *, Window)) dllXMapSubwindows)(display, w);
}

int XMapWindow(Display *display, Window w)
{
    fprintf(stdout, "[spotifywm] XMapWindow(window=0x%lx)\n", w);

    SetClassHint(display, w, "spotify", "Spotify");
    return ((int (*)(Display *, Window)) dllXMapWindow)(display, w);
}

void spotifywm_init(void) __attribute__((constructor));
void spotifywm_init(void)
{
    if (strcmp(program_invocation_short_name, "spotify"))
    {
        fprintf(stderr, "[spotifywm] Program invocation didn't match 'spotify': %s\n",
                program_invocation_short_name);
    }

    dllXCreateWindow = dlsym(RTLD_NEXT, "XCreateWindow");
    dllXCreateSimpleWindow = dlsym(RTLD_NEXT, "XCreateSimpleWindow");
    dllXMapRaised = dlsym(RTLD_NEXT, "XMapRaised");
    dllXMapSubwindows = dlsym(RTLD_NEXT, "XMapSubwindows");
    dllXMapWindow = dlsym(RTLD_NEXT, "XMapWindow");

    assert(dllXCreateWindow);
    assert(dllXCreateSimpleWindow);
    assert(dllXMapRaised);
    assert(dllXMapSubwindows);
    assert(dllXMapWindow);
}

and is compiled with:

gcc -Wall -Wextra -O2 -shared -fPIC -static-libgcc -lX11 -o spotifywm.so spotifywm.c

Long story short, booting spotify outputs the following:

/opt/spotify/spotify: /usr/lib/libcurl-gnutls.so.4: no version information available (required by /opt/spotify/spotify)
/opt/spotify/spotify: /usr/lib/libcurl-gnutls.so.4: no version information available (required by /opt/spotify/spotify)
/opt/spotify/spotify: /usr/lib/libcurl-gnutls.so.4: no version information available (required by /opt/spotify/spotify)
[spotifywm] 0x6a00001 = XCreateWindow(parent=0x79b)
/proc/self/exe: /usr/lib/libcurl-gnutls.so.4: no version information available (required by /proc/self/exe)
[spotifywm] Program invocation didn't match 'spotify': exe
[spotifywm] 0x600000a = XCreateWindow(parent=0x5e00008)
[spotifywm] XMapWindow(window=0x600000a)
[spotifywm] 0x6000012 = XCreateWindow(parent=0x5e00008)
[spotifywm] XMapWindow(window=0x6000012)

showing the creation of three windows and the mapping of two. The issue seems to be though that the parent window for the latter two create calls is somehow owned by Spotify according to xlsw -r:

0x05E00007  ---  Spotify/spotify  Spotify Premium
  0x05E00008  --o  NA           NA
    0x06000012  ---  Spotify/spotify  NA

and doesn't have it's name or class set prior to being mapped (and we don't see a mapping call being intercepted for that window ID). I've tried setting the class hint of the parent windows in the latter two mapping calls however it seems like that's too late as my bspwm rule still doesn't handle the spotify window properly.

Hoping someone more familiar with X may know how else the 0x5e00008 window in the above example could be getting created and/or mapped.

EDIT: Also potentially worth adding that xprop gives the Spotify window's id as 0x5e00008 as well, not one the the windows we see being mapped above.

By changing the code in XMapWindow to say "notspotify", I get:

0x01600007  ---  Spotify/spotify  Spotify Premium
  0x01600008  --o  NA           NA
    0x01800008  ---  Notspotify/notspotify  NA

And with awesomewm I can reload my window manager, which then reapplies all the rules and correctly send spotify to my desired tag.

That shows that the rules query 0x01600007, even though the second line is the window id from xprop (in my case it is 0x01600008), and the only window we seem to be able to affect is the bottom one.

Same on XFWM4 with devilspie2. Devilspie2 sees both the window title and the app name as "Untitled window", so here's what I'm doing to work around it. The idea is that if both values are set to that and the spotify process is less than three seconds old, it's reasonable to assume that the window belongs to Spotify. It could be reasonable to presume that Spotify is the only application on a given Linux desktop that would misbehave so flagrantly, but better safe than sorry.

if win_name == "Untitled window" and app_name == "Untitled window" then
    -- Let's find out if Spotify just started.
    handle = io.popen("pgrep -o spotify")
    proc = "/proc/"..handle:read()
    handle:close()
    handle = io.popen("stat -c%X "..proc)
    stat = handle:read()
    handle:close()
    if (os.time() - stat) < 3 then
        debug_print("Spotify just started; this window likely belongs to it.")
        set_class("Spotify")  -- Custom function to change the class for the rest of the scripts.
    end
end

hey, any update?

thx

I made an update to the tiler for pop-os that detects WM_CLASS change and then re-tiles. This same method could be used to fix other tiling wms

@msdrigg cool, how did you do? I'm using pop-os too, and it's too boring. :(

@andrevandal I submitted a pr for pop-shell here pop-os/shell#1174. You can install from github to have these changes on your system, but its not going to be available on a release for awhile.

There are some additional issues with the latest spotify flatpak, so if you do upgrade, make sure you are using the spotify snap or the regular deb.

Same on XFWM4 with devilspie2. Devilspie2 sees both the window title and the app name as "Untitled window", so here's what I'm doing to work around it. The idea is that if both values are set to that and the spotify process is less than three seconds old, it's reasonable to assume that the window belongs to Spotify. It could be reasonable to presume that Spotify is the only application on a given Linux desktop that would misbehave so flagrantly, but better safe than sorry.

if win_name == "Untitled window" and app_name == "Untitled window" then
    -- Let's find out if Spotify just started.
    handle = io.popen("pgrep -o spotify")
    proc = "/proc/"..handle:read()
    handle:close()
    handle = io.popen("stat -c%X "..proc)
    stat = handle:read()
    handle:close()
    if (os.time() - stat) < 3 then
        debug_print("Spotify just started; this window likely belongs to it.")
        set_class("Spotify")  -- Custom function to change the class for the rest of the scripts.
    end
end

Not at all an elegant solution, but I hacked a similar workaround for dwm:

	/* rule matching */
	c->isfloating = 0;
	c->tags = 0;
	XGetClassHint(dpy, c->win, &ch);
	if (0 == system("[ $(ps -o etimes= -p $(pgrep -o spotify)) -lt 3 ]")) {
		class    = ch.res_class ? ch.res_class : "Spotify";
	} else {
		class    = ch.res_class ? ch.res_class : broken;
	}
	instance = ch.res_name  ? ch.res_name  : broken;

In case I ever update it, here it is inside my build: https://github.com/BachoSeven/dwm/blob/master/dwm.c#L404

P.S. I found another window which has an initially broken class: Chromium's Task Manager, the one brought up by Shift+Tab.

interesting, where exactly do I put this code in dwm? @BachoSeven

@mohad12211 You have to edit the rule matching code, which is inside the applyrules() function in dwm.c.

@BachoSeven thanks for the workaround, but it doesn't work unfortunately. atleast on my system it doesn't.

@wael444 how exactly? did you change the code, recompile, add a rule for the Spotify class in config.h, and then restart dwm?

did you change the code, recompile

i added the snippet. and reloaded

add a rule for the Spotify class in config.h

i only checked the class name using xprop

however the rule applies to it anyway? i kinda expected WM_CLASS to change as well but in dwm it follows the rule specified. i it works...? i think.

To clarify: what I did is make it so any window which both spawns 3 seconds after a spotify binary has started, and also has a broken class, gets its class renamed to Spotify. By "class" in this context I mean the second string of the WM_CLASS array returned by the xprop utility, which you can reference in dwm's config.h by using the first column of the rules[] array.

hey @BachoSeven, when I do xprop I am getting:

instance: "spotify"
class: "Spotify"
title: "Spotify"

when I reload my dwm, Spotify gets into the designated tag, but when I try to open Spotify, it opens in the present tag only.

ig your patch would not benefit me as I am getting the correct xprop class, am I right ??

and any idea how to fix this ??

are you sure that you applied the patch correctly?
can you post the lines of code between /* rule matching */ and the for loop? @samyak039

@samyak039 regarding your last observation, the point is not that the xprop class is wrong when you check it, but that it is immediately after the application starts, which is the reason why the workaround is needed.

thanks @BachoSeven for explaining.

@mohad12211 afterwards I applied the patch, but Spotify is still not obeying the tag rule.

I added the patch mentioned by BachoSeven, in dwm.c

	/* rule matching */
	c->isfloating = 0;
	c->tags = 0;
	XGetClassHint(dpy, c->win, &ch);
	if (0 == system("[ $(ps -o etimes= -p $(pgrep -o spotify)) -lt 3 ]")) {
		class    = ch.res_class ? ch.res_class : "Spotify";
	} else {
		class    = ch.res_class ? ch.res_class : broken;
	}
	instance = ch.res_name  ? ch.res_name  : broken;

and have also added this in my config.h:

    /* class                instance  title      tags mask  isfloating  isfakefullscreen  monitor */
    { "Spotify",            NULL,     NULL,      1 << 7,        0,            1,            -1 },

but still the Spotify is opening in the current tag.

@samyak039 Not sure I can help then, however I am also using spotifywm, no idea if that's helping tho.

my only workaround at the moment is this script:

spotify &
sleep 0.5
xdotool set_window --classname Spotify --class spotify --name Spotify $(wmctrl -l | awk 'END{print $1}')

If it helps anyone, this is working for me in dwm out-of-the box (after adding the rule of course).

    {"Spotify", NULL, NULL, 1 << 3, 0, -1},

Never mind, I am running an older version of Spotify, 1.1.10

only after adding the rule and the change in dwm.c? or just the rule?

in theory this shouldn't be possible.

Sorry for the confusion, I'm running an older version of Spotify.

No!! don't update!! give us the old version!!!!!