• 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

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,609
Likes
3,528
A very basic example of a responsive web page displaying the current track on your WiiM Mini. Just install the Python requirements (xmltodict,upnpclient), update the IP address in server.py to point to your WiiM Mini, chmod +x server.py, and start it up. Point your browser to http://localhost:8080 I'm running it on a Chromebook (in a Linux instance), but anything that runs Python should work.

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":
            ####################################################################
            #### Change the ip address to that of your WiiM Mini
            d = upnpclient.Device("http://192.168.68.112:49152/description.xml")
            ####################################################################
            
            obj = d.AVTransport.GetMediaInfo(InstanceID='0')
            meta = obj['CurrentURIMetaData']
            items = xmltodict.parse(meta)["DIDL-Lite"]["item"]
            self.wfile.write(str.encode(json.dumps(items)))
            return
        
        else:
            self.path = 'wiim.html'
            return http.server.SimpleHTTPRequestHandler.do_GET(self)

# 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()

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: white;
      font-family: veranda;
      font-size: 180%;
      margin: 10px 10px 10px 25px;
    }
    div.container {
        width: 96%;
        max-width: 1200px;
        /* to center the container */
    }
    img { width: 100%; height: auto; }
    .columns {
        Width: 100%;
        max-width: 1200px;
    }
    .column {
        width:100%;
    }
    @media (min-width: 48em) {
      .column {
        width: 50%;
        float:left;
      }
      .columns {
        content: "";
        display: table;
        clear: both;
      }
    }    
    </style>
 
</head>
<body>
    <div id="myData" class="columns">
    <div id="albumcover" class="column"></div>
    <div class="column">
        <p id="title"></p>
        <p id="album"></p>
        <p id="artist"></p>
        <p id="info"></p>
    </div>
    <script>
    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;
            var bitrate = data['song:bitrate'] / 1000.0;
            el = document.getElementById("info");
            el.innerHTML = `${depth} bits / ${rate} kHz   ${bitrate} kbps`
                     
        }
    setInterval(fetchJson,1000);
    fetchJson();
    </script>
</body>
</html>

1656186965725.png
 
Last edited:

Music1969

Major Contributor
Joined
Feb 19, 2018
Messages
4,679
Likes
2,850
Hi @Ralph_Cramden

Yes trying to get this working.

So I have created a folder called wiim-web

And inside that folder I have server.py and wiim.html both stored.

With my WiiM's IP address do I need " :49152" like you've shown ?

How do I find my WiiM's port ?

And what is description.xml ? Do I change "description" ?

I load up 192.168.1.249:8080 (my Pi's IP address running both wiim.py and server.py ) on my Macbook Chrome browser and it doesn't work
 
Last edited:
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,609
Likes
3,528
515A297A-D2BD-47A5-881D-86A7144D9FEF.jpeg
Install UPnP Tool app on iPhone or Android. It will find the WiiM Mini, and will show the url you need; Base URL on the iPhone version.
 

Music1969

Major Contributor
Joined
Feb 19, 2018
Messages
4,679
Likes
2,850
View attachment 214692Install UPnP Tool app on iPhone or Android. It will find the WiiM Mini, and will show the url you need; Base URL on the iPhone version.

Ok got it

So having both server.py and wiim.html inside RPi folder wiim-web is ok?

And I should be able to use the IP address of this Pi on a different machine web browser? Not using localhost

I do run chmod +x server.py and there are no errors
 

Music1969

Major Contributor
Joined
Feb 19, 2018
Messages
4,679
Likes
2,850
Ok some troubleshooting

I now run python3 server.py and get "no module names upnpclient"

This same pi is running the 7" Winshare code so I assumed it already had upnpclient
 

Music1969

Major Contributor
Joined
Feb 19, 2018
Messages
4,679
Likes
2,850
Ok got it working!
After pip install upnpclient

Need to run sudo apt-get install libxslt-dev for some reason

Working on 85" inch Sony

Am casting Chrome browser tab to the TV which has Chromecast built-in
 
Last edited:
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,609
Likes
3,528
Yeah, I used a different upnp lib, each a better fit to its respective purpose. I guess I already had libxslt-dev installed, thanks for figuring out that requirement!
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,609
Likes
3,528
Mucking about with this a bit more, adding metadata without much pain. Most of the metadata sources require a proprietary artist_id and/or album_id to get metadata, which you can only get with an API search. Sadly, allmusic.com no longer appears to even provide an API. I'm pretty lazy, so I just added a couple of links to open new tabs using the artist name. Wikiwand and, especially, last.fm work nicely with just a name. I subscribed to last.fm to remove the ads, which makes it a great resource - very easy to navigate once it's opened in a named tab with a click. Will post code to github in a week or so, after testing/mucking about more.

Screenshot 2022-06-26 3.06.01 PM.png

Screenshot 2022-06-26 3.06.18 PM.png

Screenshot 2022-06-26 3.09.55 PM.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: white;
      font-family: veranda;
      font-size: 180%;
      margin: 10px 10px 10px 25px;
    }
    div.container {
        width: 96%;
        max-width: 1200px;
        /* to center the container */
    }
    img { width: 100%; height: auto; }
    .columns {
        Width: 100%;
        max-width: 1200px;
    }
    .column {
        width:100%;
    }
    @media (min-width: 48em) {
      .column {
        width: 50%;
        float:left;
      }
      .columns {
        content: "";
        display: table;
        clear: both;
      }
    }      
    </style>
   
</head>
<body>
    <div id="myData" class="columns">
    <div id="albumcover" class="column"></div>
    <div class="column">
            <p id="title"></p>
        <p id="album"></p>
        <p id="artist"></p>
            <p id="info"></p>
            <p id="lastfm"></p>
            <p id="wiki"></p>
        </div>
    </div>
    <script>
    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;

            var bitrate = data['song:bitrate'] / 1000.0;
            el = document.getElementById("info");
            el.innerHTML = `${depth} bits / ${rate} kHz   ${bitrate} kbps - ${actualQuality}`;
            var artist = data['upnp:artist'];
            var url = "<a href='http://last.fm/music/" + artist.replace(/ /g,"+") + "/+wiki' target='lastfm'>last.fm</a>";
            el = document.getElementById("lastfm");
            el.innerHTML = url;
            var url = "<a href='http://wikiwand.com/en/" + artist.replace(/ /g,"_") + "' target='wiki'>wiki</a>";
            el = document.getElementById("wiki");
            el.innerHTML = url;
                       
        }
    setInterval(fetchJson,3000);
    fetchJson();
    </script>
</body>
</html>
 
Last edited:

Brantome

Major Contributor
Joined
Apr 4, 2020
Messages
1,191
Likes
1,035
Got it running on my Windows PC after defrosting my brain - been too long since I messed about with things like that. Copying and tweaking VBA code into excel is about as far as I get these days, having started with Fortran IV and COBOL way back in the seventies.. ;)
 

Brantome

Major Contributor
Joined
Apr 4, 2020
Messages
1,191
Likes
1,035
Did a wee bit of googling (plagiarism rules ;) ) and added this 'I got lucky' search on duckduckgo to pull up its first search result for allmusic.com:

var url = "<a href='http://duckduckgo.com/?q=%5C" + artist.replace(/ /g,"+") + "+site:allmusic.com ' target='allmusic'>allmusic</a>";
el = document.getElementById("allmusic");
el.innerHTML = url;
<!-- https://duckduckgo.com/?q=\stephen+bishop+overview+site:allmusic.com -->

I had to add the allmusic entry as another entry further up the code. You young(er) coders can tidy this up for me :)

Update: Add "+overview" before the site item might help ensure you get the artist main page rather than discography etc
 
Last edited:
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,609
Likes
3,528
Brilliant! I’ll give it a try after I walk the dog. Allmusic really is a great resource, thanks for the clever workaround.
 

Katji

Major Contributor
Joined
Sep 26, 2017
Messages
2,990
Likes
2,273
Mucking about with this a bit more, adding metadata without much pain. Most of the metadata sources require a proprietary artist_id and/or album_id to get metadata, which you can only get with an API search. Sadly, allmusic.com no longer appears to even provide an API. I'm pretty lazy, so I just added a couple of links to open new tabs using the artist name.
Nice. I started to wonder about external dependency, then read on. Just links, so no harm if anything changes outside.

Got it running on my Windows PC after defrosting my brain - been too long since I messed about with things like that. Copying and tweaking VBA code into excel is about as far as I get these days, having started with Fortran IV and COBOL way back in the seventies.. ;)
:) Now I'm more inspired. :) I installed Python - about a year ago, I think, and then never did anything further. [embarrassed face emoji]
COBOL I only saw for a few days on a sort of introductory course, 1985/6. Then proceeded to Clipper and C and Pascal, then I stopped after beginning with C#. Then since retiring, just a few hours trying to figure out Javascipt, without any reading first. :rolleyes:
So now I'm thinking I need to get on with it - with Python. ...I was looking at the code here trying to figure out how it interfaced with WiiM, like was there an SDK interface or something. :) lol

So now with the external links, this is sort of what I do manually, after I stopped bothering about trying to automate it.
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,609
Likes
3,528
OK, updated with @Brantome's ducky workaround for all the queries - using the smarts of the innerwebs in more ways than one! Also fixed an obnoxious issue with apostrophes in the album or artist names, which was truncating the string. :facepalm: Working much better now, though I'm sure other bugs will rear their ugly heads...

This really makes Amazon Music much more usable for me - I really need the metadata, which Tidal and Qobuz supply, but Amazon has other advantages. A pretty good compromise until some vendor makes the "perfect" streaming service. ;)

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: white;
      font-family: veranda;
      font-size: 180%;
      margin: 10px 10px 10px 25px;
    }
    div.container {
        width: 96%;
        max-width: 1200px;
        /* to center the container */
    }
    img { width: 100%; height: auto; }
    .columns {
        Width: 100%;
        max-width: 1200px;
    }
    .column {
        width:100%;
    }
    @media (min-width: 48em) {
      .column {
        width: 50%;
        float:left;
      }
      .columns {
        content: "";
        display: table;
        clear: both;
      }
    }     
    </style>
  
</head>
<body>
    <div id="myData" class="columns">
    <div id="albumcover" class="column"></div>
    <div class="column">
            <p id="title"></p>
        <p id="album"></p>
        <p id="artist"></p>
            <p id="info"></p>
        <p id="allmusic"></p>
            <p id="lastfm"></p>
            <p id="wiki"></p>
        </div>
    </div>
    <script>
    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;

            var bitrate = data['song:bitrate'] / 1000.0;
            el = document.getElementById("info");
            el.innerHTML = `${depth} bits / ${rate} kHz   ${bitrate} kbps - ${actualQuality}`;
            var artist = encodeURIComponent(data['upnp:artist'].replace(/'/g,""));
            var album = encodeURIComponent(data['upnp:album'].replace(/'/g,""));
            var url = `<a href='http://duckduckgo.com/?q=%5Csite:last.fm ${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'>wiki</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(fetchJson,3000);
    fetchJson();
    </script>
</body>
</html>


Screenshot 2022-06-27 10.34.26 AM.png
 

Smitty2k1

Active Member
Joined
Jan 27, 2022
Messages
281
Likes
234
Thanks for continuing to work on this! I use LMS/PiCorePlayer/Material Skin and UPNP to play on my Wiim and so there's not probably a need for me to mess with this but I can see how it could be useful for a lot of use cases!
 
OP
Ralph_Cramden

Ralph_Cramden

Major Contributor
Joined
Dec 6, 2020
Messages
2,609
Likes
3,528
Trying my old, rusty CSS skills out - needs work, maybe a CSS expert will weigh in with something fancy...

1656364956823.png
 
Last edited:

voodooless

Grand Contributor
Forum Donor
Joined
Jun 16, 2020
Messages
10,415
Likes
18,395
Location
Netherlands
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.
 
Top Bottom