• WANTED: Happy members who like to discuss audio and other topics related to our interest. Desire to learn and share knowledge of science required. There are many reviews of audio hardware and expert members to help answer your questions. Click here to have your audio equipment measured for free!

Simple Python Web App for WiiM Mini Display

OK, last dump here before I move this to github. Some CSS changes, and much greater efficiency on the server side, now consumes about 5% of CPU on a lowly Pi Zero W. In the next week or so, will add the usual play/pause, etc. buttons.

Screenshot 2022-06-28 7.29.50 AM.png


wiim.html:
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    
    <title>WiiM Mini</title>
    
    <style>
    body {
      background-color: grey;
    }       
    h1 {
      color: blue;
      font-family: verdana;
      font-size: 300%;
    }
    p {
      color: black;
      font-family: Helvetica;
      font-size: 2em;
      margin: 50px 10px 10px 25px;
    }
    ul {
      list-style-type: none;
      margin-left: 0px;
      margin-top: 0px;
      margin-bottom: 0px;
      margin-right: 0px;
      padding: 0px;
      overflow: hidden;
      background-color: black;
    }

    li {
      float: left;
    }

    li a {
      display: block;
      color: white;
      padding: 8px;
      text-align: center;
      font-family: Helvetica;
      font-size: 180%;
      text-decoration: none;
    }

.flex-container{
    width: 100%;
    min-height: 300px;
    margin: 0 auto;
    display: -webkit-flex; /* Safari */       
    display: flex; /* Standard syntax */
}
.flex-container .column1{
        width: 48%;
    padding: 0px;
    background: grey;
    -webkit-flex: 1; /* Safari */
    -ms-flex: 1; /* IE 10 */
    flex: 1; /* Standard syntax */
}

.flex-container .column2{
        margin-left: 10px;
    -webkit-flex: 1; /* Safari */
    -ms-flex: 1; /* IE 10 */
    flex: 1; /* Standard syntax */
        border-radius: 10px;
        background-color: lightgrey;
        position: relative;
}
    #info {
        position: absolute;
        bottom: 0;
    }

    img { max-width: 100%; height: auto;}
    </style>
    
</head>
<body>
    <div id="myData" class="flex-container">
    <div id="albumcover" class="column1"></div>
    <div class="column2">
            <ul>
        <li id="allmusic"></li>
            <li id="lastfm"></li>
            <li id="wiki"></li>
            </ul>
            <p id="title"></p>
        <p id="album"></p>
        <p id="artist"></p>
            <p id="info"></p>
        </div>
    </div>
    <script>
        let oldState = '';
        function getStatus() {
      fetch('?action=status')
           .then(function (response) {
                let json = response.json();
        return json;
        })
        .then(function (data) {
              updateStatus(data);
        })
        .catch(function (err) {
        console.log('error: ' + err);
        });
        }

        function updateStatus(data) {
            state = data['CurrentTransportState'];

            if(state != oldState) {
                oldState = state;
                console.log(state);
                if(state == 'PLAYING')
                    fetchJson();
            }
        }

    function fetchJson() {
        fetch('?action=getdata')
            .then(function (response) {
                return response.json();
            })
            .then(function (data) {
                updateData(data);
            })
            .catch(function (err) {
                console.log('error: ' + err);
            });
    }


        function updateData(data) {
            var mainContainer = document.getElementById("myData");
            var div = document.getElementById("albumcover");
            div.innerHTML = '<img src="' + data['upnp:albumArtURI'] + '" style="width:100%;"</img>';
            
            var el = document.getElementById("title");
            el.innerHTML =  data['dc:title'];
            el = document.getElementById("artist");
            el.innerHTML = data['upnp:artist'];
            el = document.getElementById("album");
            el.innerHTML = data['upnp:album'];
            var depth = data['song:format_s'];
            if(depth > 24) depth=24;
            var actualQuality = data['song:actualQuality'];
            var rate = data['song:rate_hz'] / 1000.0;
            if(actualQuality == 'HD')
             depth = 16;

            if(actualQuality == 'LOSSLESS')
                actualQuality = "HiFi";

            var bitrate = data['song:bitrate'] / 1000.0;
            if (isNaN(bitrate))
              bitrate = "";
            else bitrate = bitrate + " kbps";

            el = document.getElementById("info");
            el.innerHTML = `${depth} bits / ${rate} kHz   ${bitrate} - ${actualQuality}`;
            var artist = encodeURIComponent(data['upnp:artist'].replace(/'/g,""));
            p = artist.indexOf('(');
            if(p>0)
                artist = artist.substring(0,p);

        var album = encodeURIComponent(data['upnp:album'].replace(/'/g,""));
            p = album.indexOf('(');
            if(p>0)
                album = album.substr(0,p);


            var url = `<a href='http://duckduckgo.com/?q=%5Csite:last.fm english ${artist} ${album}' target='lastfm'>Last.fm</a>`;
            el = document.getElementById("lastfm");
            el.innerHTML = url;

            var url = `<a href='http://duckduckgo.com/?q=%5Csite:wikiwand.com ${artist} ${album}' target='wiki'>Wikiwand</a>`;
            el = document.getElementById("wiki");
            el.innerHTML = url;

               var url = `<a href='http://duckduckgo.com/?q=%5Csite:allmusic.com ${artist} ${album}' target='allmusic'>Allmusic</a>`;
        el = document.getElementById("allmusic");
        el.innerHTML = url;                       

        }
    setInterval(getStatus,1000);
    getStatus();
    fetchJson();
    </script>
</body>
</html>

server.py:
Python:
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import http.server
import socketserver
from urllib.parse import urlparse
from urllib.parse import parse_qs
import json
import xmltodict
import upnpclient

class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):

    def do_GET(self):
        # Extract query param
        action = ''
        query_components = parse_qs(urlparse(self.path).query)
        if 'action' in query_components:
            action = query_components["action"][0]
            content_type = "application/json"
            
            self.send_response(200)
            self.send_header("Content-type", content_type)
            self.end_headers()

        if action == "getdata":
            obj = dev.AVTransport.GetMediaInfo(InstanceID='0')
            meta = obj['CurrentURIMetaData']
            items = xmltodict.parse(meta)["DIDL-Lite"]["item"]
            self.wfile.write(str.encode(json.dumps(items)))
            return

        if action == "status":
            obj = dev.AVTransport.GetTransportInfo(InstanceID='0')
            self.wfile.write(str.encode(json.dumps(obj)))
            return           
        
        else:
            self.path = 'wiim.html'
            return http.server.SimpleHTTPRequestHandler.do_GET(self)


####################################################################
#### Change the ip address to that of your WiiM Mini
dev = upnpclient.Device("http://192.168.68.112:49152/description.xml")
####################################################################

# Create an object of the above class
handler_object = MyHttpRequestHandler

PORT = 8080
my_server = socketserver.TCPServer(("", PORT), handler_object)

# Start the server
my_server.serve_forever()
 
First attempt to embed this as a Chrome extension, which would be very handy. Works, but can't get the window size any larger, or make it float independently. Grrrr... Have some Googling to do...


Screenshot 2022-06-28 2.27.24 PM.png
 
Well, I'll have to do a simplified version that can fit a small window. Having it available as a Chrome extension, with a click on the browser, over virtually any web page, is pretty compelling. Having the nav buttons there would be killer.

1656455400556.png
 
Simple enough to get this to work. The only change is to the two font-size CSS entries in wiim.html. Change them to:
CSS:
font-size: 2.5vw;
which will make them resize with the window width.

Then follow the instructions here to build the Chrome extension. I stole the Amazon icon from WiiM's web page.

index.html (your ip address will be different):
HTML:
<!DOCTYPE html> 
 <html>
   <body>
    <iframe src="http://192.168.68.142:8080" style="width:650px; height:330px">
   </body>
 </html>

manifest.json:
JSON:
{
  "name": "WiiM Mini",
  "description": "WiiM Mini Viewer",
  "manifest_version": 2,
  "version": "1.0",
  "browser_action": {
    "default_icon": "icon.png",
    "default_popup": "index.html"
  },
  "permissions": [
   "activeTab",
   "storage"
  ]
 }

Once your extension is installed, make it sticky so it's always visible in the browser's toobar.

Screenshot 2022-06-28 5.12.14 PM.png
 

Attachments

  • 1656461618474.png
    1656461618474.png
    21.8 KB · Views: 75
Last edited:
OK, last dump here before I move this to github. Some CSS changes, and much greater efficiency on the server side, now consumes about 5% of CPU on a lowly Pi Zero W. In the next week or so, will add the usual play/pause, etc. buttons.

View attachment 215330

wiim.html:
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
   
    <title>WiiM Mini</title>
   
    <style>
    body {
      background-color: grey;
    }      
    h1 {
      color: blue;
      font-family: verdana;
      font-size: 300%;
    }
    p {
      color: black;
      font-family: Helvetica;
      font-size: 2em;
      margin: 50px 10px 10px 25px;
    }
    ul {
      list-style-type: none;
      margin-left: 0px;
      margin-top: 0px;
      margin-bottom: 0px;
      margin-right: 0px;
      padding: 0px;
      overflow: hidden;
      background-color: black;
    }

    li {
      float: left;
    }

    li a {
      display: block;
      color: white;
      padding: 8px;
      text-align: center;
      font-family: Helvetica;
      font-size: 180%;
      text-decoration: none;
    }

.flex-container{
    width: 100%;
    min-height: 300px;
    margin: 0 auto;
    display: -webkit-flex; /* Safari */      
    display: flex; /* Standard syntax */
}
.flex-container .column1{
        width: 48%;
    padding: 0px;
    background: grey;
    -webkit-flex: 1; /* Safari */
    -ms-flex: 1; /* IE 10 */
    flex: 1; /* Standard syntax */
}

.flex-container .column2{
        margin-left: 10px;
    -webkit-flex: 1; /* Safari */
    -ms-flex: 1; /* IE 10 */
    flex: 1; /* Standard syntax */
        border-radius: 10px;
        background-color: lightgrey;
        position: relative;
}
    #info {
        position: absolute;
        bottom: 0;
    }

    img { max-width: 100%; height: auto;}
    </style>
   
</head>
<body>
    <div id="myData" class="flex-container">
    <div id="albumcover" class="column1"></div>
    <div class="column2">
            <ul>
        <li id="allmusic"></li>
            <li id="lastfm"></li>
            <li id="wiki"></li>
            </ul>
            <p id="title"></p>
        <p id="album"></p>
        <p id="artist"></p>
            <p id="info"></p>
        </div>
    </div>
    <script>
        let oldState = '';
        function getStatus() {
      fetch('?action=status')
           .then(function (response) {
                let json = response.json();
        return json;
        })
        .then(function (data) {
              updateStatus(data);
        })
        .catch(function (err) {
        console.log('error: ' + err);
        });
        }

        function updateStatus(data) {
            state = data['CurrentTransportState'];

            if(state != oldState) {
                oldState = state;
                console.log(state);
                if(state == 'PLAYING')
                    fetchJson();
            }
        }

    function fetchJson() {
        fetch('?action=getdata')
            .then(function (response) {
                return response.json();
            })
            .then(function (data) {
                updateData(data);
            })
            .catch(function (err) {
                console.log('error: ' + err);
            });
    }


        function updateData(data) {
            var mainContainer = document.getElementById("myData");
            var div = document.getElementById("albumcover");
            div.innerHTML = '<img src="' + data['upnp:albumArtURI'] + '" style="width:100%;"</img>';
           
            var el = document.getElementById("title");
            el.innerHTML =  data['dc:title'];
            el = document.getElementById("artist");
            el.innerHTML = data['upnp:artist'];
            el = document.getElementById("album");
            el.innerHTML = data['upnp:album'];
            var depth = data['song:format_s'];
            if(depth > 24) depth=24;
            var actualQuality = data['song:actualQuality'];
            var rate = data['song:rate_hz'] / 1000.0;
            if(actualQuality == 'HD')
             depth = 16;

            if(actualQuality == 'LOSSLESS')
                actualQuality = "HiFi";

            var bitrate = data['song:bitrate'] / 1000.0;
            if (isNaN(bitrate))
              bitrate = "";
            else bitrate = bitrate + " kbps";

            el = document.getElementById("info");
            el.innerHTML = `${depth} bits / ${rate} kHz   ${bitrate} - ${actualQuality}`;
            var artist = encodeURIComponent(data['upnp:artist'].replace(/'/g,""));
            p = artist.indexOf('(');
            if(p>0)
                artist = artist.substring(0,p);

        var album = encodeURIComponent(data['upnp:album'].replace(/'/g,""));
            p = album.indexOf('(');
            if(p>0)
                album = album.substr(0,p);


            var url = `<a href='http://duckduckgo.com/?q=%5Csite:last.fm english ${artist} ${album}' target='lastfm'>Last.fm</a>`;
            el = document.getElementById("lastfm");
            el.innerHTML = url;

            var url = `<a href='http://duckduckgo.com/?q=%5Csite:wikiwand.com ${artist} ${album}' target='wiki'>Wikiwand</a>`;
            el = document.getElementById("wiki");
            el.innerHTML = url;

               var url = `<a href='http://duckduckgo.com/?q=%5Csite:allmusic.com ${artist} ${album}' target='allmusic'>Allmusic</a>`;
        el = document.getElementById("allmusic");
        el.innerHTML = url;                      

        }
    setInterval(getStatus,1000);
    getStatus();
    fetchJson();
    </script>
</body>
</html>

server.py:
Python:
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import http.server
import socketserver
from urllib.parse import urlparse
from urllib.parse import parse_qs
import json
import xmltodict
import upnpclient

class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):

    def do_GET(self):
        # Extract query param
        action = ''
        query_components = parse_qs(urlparse(self.path).query)
        if 'action' in query_components:
            action = query_components["action"][0]
            content_type = "application/json"
           
            self.send_response(200)
            self.send_header("Content-type", content_type)
            self.end_headers()

        if action == "getdata":
            obj = dev.AVTransport.GetMediaInfo(InstanceID='0')
            meta = obj['CurrentURIMetaData']
            items = xmltodict.parse(meta)["DIDL-Lite"]["item"]
            self.wfile.write(str.encode(json.dumps(items)))
            return

        if action == "status":
            obj = dev.AVTransport.GetTransportInfo(InstanceID='0')
            self.wfile.write(str.encode(json.dumps(obj)))
            return          
       
        else:
            self.path = 'wiim.html'
            return http.server.SimpleHTTPRequestHandler.do_GET(self)


####################################################################
#### Change the ip address to that of your WiiM Mini
dev = upnpclient.Device("http://192.168.68.112:49152/description.xml")
####################################################################

# Create an object of the above class
handler_object = MyHttpRequestHandler

PORT = 8080
my_server = socketserver.TCPServer(("", PORT), handler_object)

# Start the server
my_server.serve_forever()
Just added this new code

I don't seem to be able to get it to autoboot now (it was before).

I can manually run sudo python3 server.py and it works

But it "fails to start" on reboot. Has anything changed that could affect this ? See attached screenshots

This was working fine before I updated todays server.py code :
1656497527351.png

I do run systemctl enable and start and restart wiimweb.service but no luck :

1656497572548.png
 
So after reboot I check the status of wiim-web (I deleted wiimweb.service just now and created wiim-web.service because its easier for me to remember since folder is wiim-web

This is status

1656505294705.png
 
Use journalctl to see the logs of a service:

sudo journalctl -u wiimweb.service

Dumb Q: You did update the ip address in the Python script, right?
 
Use journalctl to see the logs of a service:

sudo journalctl -u wiimweb.service
thanks, will do

Dumb Q: You did update the ip address in the Python script, right?

Good question but yes. That's why when I manually run "sudo python3 server.py" it works properly

Problem is the service on reboot for automatically starting up serve.py
 
Actually, you may not need the webserver at all and use a static page that you can host anywhere. You can probably just read the XML file in the browser and process it just fine (this would work if the XML serving server has the proper CROS headers). You could have an input screen for the URL, and store it in local storage, and next time read it from there. I don't have a WiiM Mini so I can't check if this would be possible.
It's possible, has been done as a Chrome extension here. If someone has the node.js chops, this could be modified to talk to the WiiM via its UPnP interface. Lotta work involved, though.
 
Try running it manually. In the wiim-web directory, type:

./server.py

Make sure this line is first line in server.py:

#!/usr/bin/python3
 
Also make sure your service starts script as pi user, not root
 
Just added this new code

I don't seem to be able to get it to autoboot now (it was before).

I can manually run sudo python3 server.py and it works

But it "fails to start" on reboot. Has anything changed that could affect this ? See attached screenshots

This was working fine before I updated todays server.py code :
View attachment 215505

I do run systemctl enable and start and restart wiimweb.service but no luck :

View attachment 215506
Have you tried to use this : ExecStart=/usr/bin/python3 /home/pi/wiim-web/server.py ?
 
Have you tried to use this : ExecStart=/usr/bin/python3 /home/pi/wiim-web/server.py ?
This fixed it

I knew that manually running this was working but wasn aware how to make it fit in that ExecStart line. I had tried a few things yesterday but obv was getting the syntax wrong

Thanks for this !!
 
This fixed it

I knew that manually running this was working but wasn aware how to make it fit in that ExecStart line. I had tried a few things yesterday but obv was getting the syntax wrong

Thanks for this !!
glad to be of help coz my current project involves working with python and I had to manage getting them running with such services
 
This should work fine on any OS. The Bullseye limitation had to do with the Waveshare screen, which this doesn't use.
 
This should work fine on any OS. The Bullseye limitation had to do with the Waveshare screen, which this doesn't use.
Sorry I posted in the wrong thread.

My post about Bullseye should be in the Waveshare display thread

Moved it there

HTML page is lookin great !

I got the app called AirBrowser for iPad and it puts the full page on an Apple TV 85" screen

Only issue is at that size TV (or even 60") the album cover resolution isn't great

But obviously fine resolution for 7inch display. I got a few around the house now !
 
Last edited:
Back
Top Bottom