Skip to content

Instantly share code, notes, and snippets.

@aiguofer
Last active May 6, 2022 20:35
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aiguofer/c3644b16f4aed3070baf84de5423045f to your computer and use it in GitHub Desktop.
Save aiguofer/c3644b16f4aed3070baf84de5423045f to your computer and use it in GitHub Desktop.
Polybar module for Google Play Music Desktop Player

I've been making an effort for a while to hit like on songs I like, mainly so I can always fail over to a playlist of liked songs. I generally use GPMDP as my main player and Polybar as my taskbar. I control my media player using playerctl and a while back I wrote songliker to set up a shortcut to like my songs. I decided to put it all together into a neat little polybar module and thought others might enjoy this.

I'm including the necessary files to do this yourself, although this might not work for you 'as-is' since I have a pretty customized systemd service management set up. Easiest way would be to just ignore the systemd stuff and just add an exec now_playing, but I'm including it here in case you want to use it and just modify that.

This will display the currently playing song in your bar in the following format {artist} - {song} | {liked?}. It uses Nerd Font symbols for a prefixing musical note and a thumbs up outline if not liked, or full if liked. It will also truncate the song name to fit the whole message into MAX_LEN characters.

It has the following inteactions:

  • Left Click - Play/Pause
  • Right Click - Next Song
  • Double Click - Like Song

Here's what it looks like: image

Requirements

  • Set up polybar to use some type of Nerd Fonts, or include those symbols, or adjust the symbols to your liking
  • Install playerctl
  • Install any missing python dependencies (might just be websockets
  • Run songliker once on the command line to register it against GPMDP. It'll prompt you for a code, but once you've set it up you won't have to worry about it again since the auth is saved.
// ~/.config/polybar/config
// only including relevant section
...
enable-ipc = true
...
[module/now_playing_gpmdp]
type = custom/ipc
hook-0 = echo ""
hook-1 = cat "${HOME}/.now_playing"
format = <output>
format-prefix = " "
format-prefix-foreground = ${colors.foreground}
click-left = playerctl --player=google-play-music-desktop-player play-pause
click-right = playerctl --player=google-play-music-desktop-player next
double-click-left = songliker
#!/usr/bin/env python
# -*_ coding: utf-8 -*-
# ~/.local/bin/now_playing
import json
import signal
import subprocess
import sys
from pathlib import Path
from time import sleep
import websocket
# TODO: change these to args?
MAX_LEN = 60
MODULE_NAME = "now_playing"
NOW_PLAYING_FILE = Path("~/.now_playing").expanduser()
now_playing_data = {}
def handle_message(ws, message):
global now_playing_data
if any(
'"channel":"{0}"'.format(channel) in message for channel in ["track", "rating"]
):
msg = json.loads(message)
now_playing_data.update(msg["payload"])
# track messages always come in first, update once we get rating message
if now_playing_data["title"] and msg["channel"] == "rating":
send_curr_song()
def truncate_title_if_needed():
# 7 hardcoded chars
str_len = 7 + sum(
len(v) for k, v in now_playing_data.items() if k in ["artist", "title"]
)
if str_len > MAX_LEN:
# 3 chars for ellipsis
to_remove = MAX_LEN - str_len - 3
now_playing_data["title"] = now_playing_data["title"][:to_remove] + "..."
def send_curr_song():
now_playing_data["liked"] = "" if now_playing_data["liked"] else ""
truncate_title_if_needed()
msg = "{artist} - {title} | {liked}".format(**now_playing_data)
NOW_PLAYING_FILE.write_text(msg)
send_polybar(2)
def clear_song():
send_polybar(1)
def send_polybar(hook):
subprocess.call(["polybar-msg", "hook", MODULE_NAME, str(hook)])
def signal_handler(sig, frame):
clear_song()
sys.exit(0)
def main():
while True:
signal.signal(signal.SIGTERM, signal_handler)
ws = websocket.WebSocketApp("ws://localhost:5672", on_message=handle_message)
ws.run_forever()
clear_song()
sleep(5)
if __name__ == "__main__":
main()
# ~/.local/share/systemd/user/now_playing.service
[Unit]
Description=watch for song changes and tell polybar
After=polybar.service
PartOf=polybar.service
[Service]
ExecStart=/bin/bash -c now_playing
[Install]
WantedBy=i3.target
#!/usr/bin/env python
# -*_ coding: utf-8 -*-
import json
from pathlib import Path
from subprocess import call
import websocket
config_location = Path("~/.config/gpmdp_api").expanduser()
def like_song(ws):
thumbs_up = {"namespace": "rating", "method": "setRating", "arguments": [5]}
ws.send(json.dumps(thumbs_up))
try:
call(["notify-send", "Liked song on GPMDP"])
except FileNotFoundError:
call(["terminal-notifier", "-message", "Liked song on GPMDP"])
def get_saved_code():
if config_location.exists():
return config_location.read_text()
def save_code(code):
with open(config_location, "w") as conf:
conf.write(code)
def get_code(ws):
code = get_saved_code()
if not code:
code = authenticate(ws)
return code
def authenticate(ws):
connect(ws)
code = input("Please input code: ")
res = connect(ws, code)
save_code(res)
return res
def connect(ws, code=None):
connect = {"namespace": "connect", "method": "connect", "arguments": ["Song Liker"]}
if code is not None:
connect["arguments"].append(str(code))
ws.send(json.dumps(connect))
# we only get a response while authenticating
if code is None or len(code) == 4:
return get_response(ws, "connect")["payload"]
def get_response(ws, channel):
for res in ws:
if '"channel":"{0}"'.format(channel) in res:
return json.loads(res)
def main():
ws = websocket.create_connection("ws://localhost:5672")
code = get_code(ws)
connect(ws, code)
like_song(ws)
ws.close()
if __name__ == "__main__":
main()
@aiguofer
Copy link
Author

Added signal handler to clear song on exit (using SIGTERM which is the systemd default... not sure if this is the best overall signal tho)

@calops
Copy link

calops commented Jun 11, 2020

Seems like the function save_code is missing from songliker.

@aiguofer
Copy link
Author

@calops yep, you're right! I had updated this in my dotfiles (https://github.com/aiguofer/dotfiles/blob/master/user/.local/bin/songliker) but forgot this gist was out there haha. I've updated it with the latest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment