Building a Music Controlling Python Start Page for Linux

Robert Washbourne - a month ago - programming, themes

By using the Gnome GLib apis, we can use the Playerctl Linux package with Python. Bundling this with bottle, a simple Python server, and websockets to listen for updates, we have a music controlling homepage.

Dependencies

You will of course need Python. Here, I use python2 because gevent (the websocket) does not support Python3.

Linux packages you need:

Python dependencies (install with pip):

  • bottle
  • bottle-websocket

Github

You can download all the code for this project on DevPy's github here.

You can clone this to your computer with

git clone https://github.com/devpytech/musicpage.git  

Adding a websocket

First we need to import the (extensive) list of packages.

#!/usr/bin/env python2
from bottle.ext.websocket import GeventWebSocketServer  
import gi  
gi.require_version('Playerctl', '1.0')  
from gi.repository import GLib, GObject, Playerctl  
from bottle.ext.websocket import websocket  
from geventwebsocket.exceptions import WebSocketError  
from bottle import run, get  
import threading  
import os  
from gevent import monkey  
monkey.patch_all()

print("starting websocket")  

Adding a route

When the main page connects to the websocket, we send the attributes so we can update the display.

@get('/websocket', apply=[websocket]) #provide a websocket connection
def echo(ws):  
    print("connected")
    # send the music status
    ws.send('%s,%s,%s,%s' % (os.popen("playerctl metadata mpris:artUrl").read(), Playerctl.Player().get_title(), Playerctl.Player().get_artist(), Playerctl.Player().get_property("status")))

Playerctl process

We can make a new Playerctl player with player = Playerctl.Player() and run a loop to check for updates with player.on('metadata', on_track_change) and GLib.MainLoop().run(). When the track is updated, we send a message to the webpage.

@get('/websocket', apply=[websocket])
def echo(ws):  
    print("connected")
    # send the music status
    ws.send('%s,%s,%s,%s' % (os.popen("playerctl metadata mpris:artUrl").read(), Playerctl.Player().get_title(), Playerctl.Player().get_artist(), Playerctl.Player().get_property("status")))
    def on_track_change(player, e):
        #this function will run when the music is updated (paused, skipped, etc.)
        try: #see if the connection is still open
            ws.send('%s,%s,%s,%s' % (os.popen("playerctl metadata mpris:artUrl").read(), Playerctl.Player().get_title(), Playerctl.Player().get_artist(), Playerctl.Player().get_property("status")))
        except WebSocketError: #if it's not open, stop the process
            player.stop()
            loop.quit()
            ws.close()

    player = Playerctl.Player()
    player.on('metadata', on_track_change) #listen for music updates

    loop = GLib.MainLoop() 
    loop.run() #run the playerctl updater
    print("Disconnected from websocket")

run(host='0.0.0.0', port=7000, server=GeventWebSocketServer) #run the server  

Frontend

In this section, we create the webpage that is shown (and updated in real time) for the user.

Settings

Here we add color choosing and links that we use in html later.

#!/usr/bin/env python2
from bottle import route, run, request, get, post, static_file  
import gi  
gi.require_version('Playerctl', '1.0')  
from gi.repository import GLib, GObject, Playerctl  
from subprocess import Popen  
import sys  
import os

Popen([sys.executable, './sendup.py']) #run the websocket script in the same folder (used to update info)

############
# SETTINGS
############

links = [['DevPy','http://DevPy.me'],['Reddit','http://Reddit.com'],['Google','http://Google.com']] #List of links to add to page

theme = 'dark' #light or dark theme (sets colors)

############
# /SETTINGS
############

if theme == 'dark':  
    bgc = '#222'
    fgc = 'rgba(255,255,255,0.6)'
    bw = 'gray'
else:  
    bgc = '#fff'
    fgc = '#111'
    bw = 'black'

for x in range(len(links)):  
    #create list of links
    links[x] = '<a class="db link no-underline underline-hover '+bw+'" href="'+links[x][1]+'">'+links[x][0]+'</a>'

linkhtml = '''<div class="db center" style="width: 182px;"><div class="mt4 lh-copy">'''

#add the links together
for x in links:  
    linkhtml += x

linkhtml += '''</div></div>'''  

HTML

We generate the initial look of the app. The artist, song, and image are initialized as well.

@get('/') # or @route('/login')
def index():  
    player = Playerctl.Player() #grab a player
    try: #Check if a music app is open (spotify, audacious, vlc,...)
        title = player.get_title()
        artist = player.get_artist()
        img = os.popen("playerctl metadata mpris:artUrl").read()
    except: #if nothing is open just show blank music
        title = ''
        artist = ''
        img = ''

    return '''
<html>  
<head>  
<title>Start Page</title>  
<link rel="stylesheet" href="/static/tachyons.css" />  
<style>  
/* Here we add the icons for play/pause/next/forward */
@font-face {
  font-family: 'Material Icons';
  font-style: normal;
  font-weight: 400;
  src: local('Material Icons'), local('MaterialIcons-Regular'), url(/static/mat.woff2) format('woff2');
}
.material-icons {
  cursor: pointer;
  font-family: 'Material Icons';
  font-weight: normal;
  font-style: normal;
  font-size: 30px;
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  display: inline-block;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  -webkit-font-feature-settings: 'liga';
  -webkit-font-smoothing: antialiased;
}
.material-icons.md-light { color: rgba(0, 0, 0, 0.54); }
.material-icons.md-dark { color: rgba(255, 255, 255, 1); }
</style>  
</head>  
<body style="background-color:'''+bgc+'''">  
<div style="position:relative; top: 35%%; transform: translateY(-50%%); ">  
<div class="mt4 db center black link" style="width: 178px;">  
  <img id="img" style="width:180px;height:178px;background-color:#222;"class="db ba b--black-10" src="%s">
  <dl class="mt2 lh-copy">
    <dt class="clip">Title</dt>
    <dd id="title" class="ml0 fw9" style="color:''' % (img)+fgc+'''">%s</dd>
    <dt class="clip">Artist</dt>
    <dd id="artist" class="ml0 gray">%s</dd>
  </dl>
  <div style="text-align:center;color:''' % (title, artist)+fgc+'''">
    <a onclick="previous();" style="float:left;"><i class="material-icons md-'''+theme+'''">skip_previous</i></a>
    <a onclick="toggle();"><i id ="stateicon" style="width:32px;" class="material-icons md-'''+theme+'''">pause_arrow</i></a>
    <a onclick="next();" style="float:right;"><i class="material-icons md-'''+theme+'''">skip_next</i></a>
  </div>
</div>  
''' + linkhtml + # add the scripts below here  

Scripts

Here we add some jquery scripts to talk to the server.

# attach this to the previous html code
'''  
</div>  
<script src="/static/jquery-3.1.1.min.js"></script>  
<script type="text/javascript">  
var ws;  
ws = new WebSocket("ws://0.0.0.0:7000/websocket");  
ws.onopen = function (evt) {  
  ws.send("Connected");
};
ws.onclose = function (evt) {  
  ws.send("Closed");
};

//in this function we listen for updates from the server and write them to the page.
ws.onmessage = function (evt) {  
    var array = evt.data.split(',');
    if (array.length == 4) {
        console.log(array);
        $("#img").attr("src", array[0]);
        $("#title").text(array[1]);
        $("#artist").text(array[2]);
        if (array[3] === 'Playing') {
            $('#stateicon').text('pause');
        } else {
            $('#stateicon').text('play_arrow');
        }
        ws.send("Updated");
    } else {
        ws.send("OK.")
    }
};
</script>  
<script>  
// when the buttons are pressed, post the server
function next() {  
    $.post('/next')
    return false;
}
function previous() {  
    $.post('/previous')
    return false;
}
function toggle() {  
    $.post('/toggle');
    return false;
}
</script>  
</body>  
</html>  
'''  

Post replies

Control the music when the client POSTs the server.

@post('/next')
def next_song():  
    Playerctl.Player().next()

@post('/previous')
def next_song():  
    Playerctl.Player().previous()

@post('/toggle')
def next_song():  
    Playerctl.Player().play_pause()

@post('/status')
def is_playing():  
    return Playerctl.Player().get_property("status")

@route('/static/<filename:path>') #This is for static files
def send_static(filename):  
    return static_file(filename, root=os.path.dirname(os.path.realpath(' ')))

run(host='0.0.0.0', port=8080) #run the server  

Finishing up

Now you should have a nice music controlling start page. You can make this show as the new tab on Chrome with this extension, and on Firefox with this one. Just set the url to 0.0.0.0:8080.

Here I use my method to show a music visualizer on the desktop, plus my openbox setup.

Youtube video

Subscribe to DevPy
Get a post every few weeks, no spam.