I am a perfectionist who loves order in everything. Most of all I am pleased when things work exactly as they should work (in my, of course, understanding). And I have long had my own personal Internet radio based on IceCast-KH + LiquidSoap. And for many years I wasnāt allowed to sleep in peace the fact that streaming broadcasting servers do not know how to give the covers (artwork) of the tracks being played in the stream. And not only in the stream - they can not do it at all. I went to IceCast-KH (fork from IceCast2) only because of one of its uber-features - he can give mp3-tags inside the flv stream (this is needed to display the executable track when playing online on the site via a flash player). And now it's time to close the last question - the release of the covers of the tracks being played - and calm down. Since there were no ready-made solutions, I didnāt think of anything better than writing my own cover server for .mp3 files. How? Welcome under cat.
Prehistory
I usually listen to the radio in the car, on a 2-din tape recorder based on Android 4.4 KitKat (and at home on a tablet under the same Android). To listen, after a long and thoughtful search of existing programs, XiiaLive was chosen, mainly because it knows how to use custom radio stations (such a banal, seemingly feature, but not supported by most streaming radio players - hereās the ShoutCast / Uber Stations catalog - choose and listen what they give), as well as for the fact that he can pump up and display the covers of the tracks being played. Yes, of course, not all, but can. The music played, the covers were partially shown and for some time the inner perfectionist calmed down, but as it turned out, not for long.
After a while, an extremely unpleasant application bug related to incorrect processing of Unicode emerged - if the track name and artist were not in Latin, the album cover was displayed incorrectly. Not only that - always the same. And I will tell you even more - for some reason this has always been Nyusha. This is what I could not tolerate.
Screenshot illustrating how XiiaLive encroached on the holy.')
It would be possible to wait until the developers fix this
bug , but sensibly judging that they hardly have covers for everything that is in rotation exactly at my station (they definitely won't have covers for Ishome, Interior Disposition, tmtnsft and more than MĪ£ $ ā ĪMN Ī£KCĆNĪ ā ), it seemed more correct to write my api for the covers. Which will be able to work precisely on the local database of music files and, if possible, without being tied to a specific broadcast server.
Investigate the issue
It was not possible to find a description of the standard protocol for the return of the covers (I assume that there is no single standard at all), so I decided to go from the opposite - to see how this is done for big guys, in particular for the same XiiaLive. Armed with
Packet Capture on Android, we catch packages and see where the application goes and why:
GET /songart.php?partner_token=7144969234&title=Umbrella&artist=The+Baseballs&res=hi HTTP/1.1 User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; QuadCore-R16 Build/KVT49L) Host: api.dar.fm Connection: Keep-Alive Accept-Encoding: gzip HTTP/1.1 200 OK Server: Apache/2.2.15 (CentOS) X-Powered-By: PHP/5.3.3 Set-Cookie: PHPSESSID=u5sgs13h1315k9184nvvutaf33; expires=Fri, 03-Aug-2018 18:39:08 GMT; path=/; domain=.dar.fm Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Pragma: no-cache X-Train: wreck="mofas16" Content-Type: application/xml; charset=UTF-8 Content-Length: 57 Accept-Ranges: bytes Date: Thu, 03 Aug 2017 18:39:08 GMT Via: 1.1 varnish Age: 0 Connection: keep-alive X-Served-By: cache-ams4143-AMS X-Cache: MISS X-Cache-Hits: 0 X-Timer: S1501785548.973935,VS0,VE390
It turned out that a normal GET request with four variables is being sent:
- partner_token - authorization token, when requested without it, or with the wrong token - 403 is returned.
- title - track title
- artist - artist name
- res - the desired resolution of the image. A simple search gave the following set of issued permissions (square covers, so the resolution is described by one number):
* hi - 1080 px
* low - 250 px
* in all other cases - 400 px
In response to a request, the application expects the following xml in response:
<?xml version="1.0" encoding="UTF-8" ?> <songs> <song> <arturl>http://coverartarchive.org/release/c8b16143-e87e-440d-bbb2-5c96615bed2b/2098621288-500.jpg </arturl> <artist>The Baseballs</artist> <title>Tik Tok</title> <album></album> <size>1080</size> </song> </songs>
And the next request, the application is expected to go to the statics server behind the picture. If the āArtistā + āTrack Nameā combination didnāt find anything, then an empty xml is returned:
<?xml version="1.0" encoding="UTF-8" ?> <songs> </songs>
Design
Okay, the input and output parameters of the black box are defined, it remains to build the logic of its work. And most importantly - decide where we will get the cover for the requested track.
To make a separate database of cover images, somehow link it with the tracks, keep it up to date - I'm lazy. Yes, and you should not multiply entities, create any extra bases and links, because the ID3v2 mp3 tags format supports storing covers in mp3 files for many years, so weāll go inside the file behind the cover (if itās there, of course, there is). And if the file is not found (or there is no cover in it), then instead of an empty xml, we'd rather give one of the default covers for the radio station, so that the user does not look in the empty square.
In general, I prefer to script everything and do less work with my hands. For example, how now adding a file to the rotation looks like: threw the file via ftp / scp into the inbox directory and forgot. A minute later, the maintenance script arrived, found the file, renamed it as needed, and put it in the radio station directory. And once every 10 minutes, LiquidSoap will reread the directory, find a new file and add it to the playlist. A request for a cover will come - the script will find the file and extract the cover from it.
A good system administrator even automatically Sysadmin Day.
By cron
The truth in the process of implementation and testing of the logic is somewhat complicated. After all, there is often still a cover.jpg in the album catalog (for performers who are present in rotation in whole albums). And there are still numerous performers from SoundCloud / PromoDJ and simply from vk who rarely collect tracks into albums, or even care about the issue of the cover for the track. For these artists (there are not so many of them), we will create a separate directory on the static server with default covers by the artistās name.
Last question: how to find the corresponding requested tags file on the disk, given that at the time of the start of the search, we only have the name of the artist and the name of the track? You can store information somewhere in the database using the keys āartist, track -> file on diskā, you can go through the files, look at them by comparing the mp3 tags with the query (but this is a long time), but you can simply follow the principle of not multiplying the entities store files on disk with names like "% artist% -% title% .mp3". I have done just that. Once for this, I used the best, in my opinion, for these purposes, the program
TagScanner from Sergey Serkov, and then switched to a python script that automatically renames files in the desired format.
The final logic of the work was as follows:
- Received a GET request.
- If the request is empty (does not contain GET parameters) - returns empty XML
- If authorization by tokens is enabled (non-zero tokens list in the configuration file), the incoming token is checked. If the token is invalid - 401 Unauthorized.
- If the request contains the variables artist and title, a search is performed in the local directory of mp3 files:
- If the file is not found, empty XML is returned.
- If the file is found, the sequence is as follows:
- Check if there is a ready-made cover for this file in the cover catalog? If there is - give a link to it.
- If there is a cover in the file, we extract it to the catalog with covers, give the link.
- If in the catalog with the .mp3 file there is an album cover (file cover.jpg) - transfer it to the album cover catalog, give a link to it.
- If the catalog of artists has a cover with the name `artist` - we give a link to it.
- If nothing is found at all, we give a random picture from the catalog of default covers of the radio station.
And now, when the logic of work is defined, it remains only to formulate it in the form of functions.
Code
To extract covers from mp3 files, use the mutagen module. The function that extracts covers from mp3 files and writes them in .jpg:
import mutagen.mp3 def extract_cover(local_file, covers_dir, cover_name): """ Extracts cover art from mp3 file :param local_file: file name (with path) :param covers_dir: path to store cover art files :param cover_name: name for extracted cover art file :rtype: bool :return: False - file not found or contains no cover art True - all ok, cover extracted """ try: tags = mutagen.mp3.Open(local_file) data = "" for i in tags: if i.startswith("APIC"): data = tags[i].data break if not data: return False else: with open(covers_dir + cover_name, "w") as cover: cover.write(data) return True except: logging.error('extract_cover: File \"%s\" not found in %s', local_file, covers_dir) return False
If there is a cover in the file and we have successfully extracted it - we make a resize for the required dimensions while preserving the proportions of the picture (for standard square covers are not always in the file). Python Imaging Library (PIL), which is also capable of antialias:
from PIL import Image def resize_image(image_file, new_size): """ Resizes image keeping aspect ratio :param image_file: file name (with full path) :param new_size: new file max size :rtype bool :return: False - resize unsuccessful or file not found True - otherwise """ try: img = Image.open(image_file) except: return False if max(img.size) != new_size: k = float(new_size) / float(max(img.size)) new_img = img.resize(tuple([int(k * x) for x in img.size]), Image.ANTIALIAS) img.close() new_img.save(image_file) return True
Despite the fact that almost all modern programs are able to customize the size of the cover to fit the screen size, I would highly recommend doing it yourself, on the server side.
In my practice, there was a case when a half of a 15 megabyte .mp3 file (7.62 mb) was occupied by a 3508x3508 cover with a non-standard color profile. This file tightly hung TagScanner program, which I use to edit tags. I do not know how much this file would be sent via 3G connection, and what would happen with Android when I tried to adjust it to the screen size.
Since XiiaLive has no settings for selecting the cover server, it was necessary to change the address of api.dar.fm to which it refers to its own. On Android, it's simple:
/etc/hosts <my_api_ip> api.dar.fm
And we explain to Nginx that all incoming requests, regardless of where they came from or what they want, are served by our script. At the same time, we are raising a virtual host for statics, from where pictures will be given. Of course, you can do everything within a single host, but still it is better to
fly api separately, and
burgers static - separately.
upstream fcgiwrap_factory { server unix:/run/fcgiwrap.socket; keepalive 32; } server { listen 80; server_name api.<yourserver> api.dar.fm; root /var/wwws/<yourserver>/api; access_log /var/log/nginx/api.access.log main; error_log /var/log/nginx/api.error.log; location / { try_files $uri /api.py?$args; } location ~ api.py { fastcgi_pass fcgiwrap_factory; include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } } server { listen 80; server_name static.<yourserver> root /var/wwws/<yourserver>/static; access_log /var/log/nginx/static.access.log main; error_log /var/log/nginx/static.error.log; index index.html; location / { } }
After fixing bugs and finishing thin spots, everything worked. Music plays, pictures are extracted from mp3 files and added to the host directory with statics for upload via the web. In theory, after a while, all the covers will move from the depths of mp3 files to the static directory, but, firstly, the process of extracting the cover takes an average of 100 ms, and secondly, the place on the hosting is not rubber, so the pictures will that time is deleted by the simplest one-liner on the bash, which hangs itself in the crown and deletes files that were accessed more than a week ago:
find /var/wwws/<yourserver>/static/covers/ -maxdepth 1 -type f -iname '*.jpg' -atime +7 -exec rm {} \;
Of course, for this to work, noatime should not be installed on the music section.
Well, it all worked, as it should work.Revision
A week later, I analyzed the server logs and found something interesting: right after launch, the application sends a request of the form:
GET /songart.php?partner_token=7144969234&res=hi HTTP/1.1" 200 334 "-" "Dalvik/1.6.0 (Linux; U; Android 4.4.2; QuadCore-R16 Build/KVT49L)
And only some time later:
GET /songart.php?partner_token=7144969234&title=Summer+Nights&artist=John+Travolta+and+Olivia+Newton-John&res=hi HTTP/1.1" 200 334 "-" "Dalvik/1.6.0 (Linux; U; Android 4.4.2; QuadCore-R16 Build/KVT49L)
Accordingly, there is no cover on the screen between these two requests, darkness and despondency.
Disorder.
The reason is clear: the application has not yet managed to extract tags from the stream and does not know what is playing, why not help it? Add the first item to another condition in the logic of the program:
- If a GET request comes with an authorization token, but without specifying the artist and track name, give the picture for the currently playing track. If there is a variable stream - from the requested stream of broadcasting, otherwise - from the one that we consider the main one.
But where to get the name of the current track? Do not get the same server logs. It is very successful that Icecast can give the state of the mounted points in XML or JSON format. JSON for Python is more native, so we will use it. Since in Icecast-KH, there are no such statistics out of the box, we will use the xsl file from the article by the respected
namikiri , which is insensitively modified by me:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:output omit-xml-declaration="yes" method="text" doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" indent="no" encoding="UTF-8" media-type="application/json; charset=utf-8"/> <xsl:strip-space elements="*"/> <xsl:template match="/icestats"> { <xsl:for-each select="source"> "<xsl:value-of select="@mount"/>": { "name" : "<xsl:value-of select="server_name"/>", "listeners" : "<xsl:value-of select="listeners"/>", "listener_peak" : "<xsl:value-of select="listener_peak"/>", "description" : "<xsl:value-of select="server_description"/>", "title" : "<xsl:value-of select="title"/>", "genre" : "<xsl:value-of select="genre"/>", "url" : "<xsl:value-of select="server_url"/>" } <xsl:if test="position() != last()"> <xsl:text>,</xsl:text> </xsl:if> </xsl:for-each> } </xsl:template> </xsl:stylesheet>
We put the file in the Icecast-kh web directory (on Ubuntu, the default is / usr / local / share / icecast / web /), and when accessing via http, we get something like this in reply:
{ "/256": { "name" : "Radio /256kbps", "listeners" : "2", "listener_peak" : "5", "description" : "mp3, 265kbit", "title" : "The Kelly Family - Fell In Love With An Alien", "genre" : "Various", "url" : "" }, "/128": { "name" : "Radio /128kbps", "listeners" : "0", "listener_peak" : "1", "description" : "mp3, 128kbit", "title" : "The Kelly Family - Fell In Love With An Alien", "genre" : "Various", "url" : "" }, "/64": { "name" : "Radio /64kbps", "listeners" : "0", "listener_peak" : "2", "description" : "mp3, 64kbit", "title" : "The Kelly Family - Fell In Love With An Alien", "genre" : "Various", "url" : "" } }
As you can see, the radio has three mount points (in fact, some more) broadcasting the same stream, but with different quality. Well, then everything is quite simple:
import urllib2 import json def get_now_playing(stats_url, stats_stream): """ Retruns current playing song - artist and title :param stats_url: url points to icecast stats url (JSON format) :param stats_stream: main stream to get info :return: string "Artist - Title" """ try: stats = json.loads(urllib2.urlopen(stats_url).read()) except: logging.error('get_current_song: Can not open stats url \"%s\"', stats_url) return False if stats_stream not in stats: logging.error('get_current_song: Can not find stream \"%s\" in stats data', stats_stream) return False return stats[stats_stream]['title'].encode("utf-8")
The function walks to the specified statistics address, and returns the artist and the title of the current composition from the desired stream. The stream comes or in request, or default (from adjustments) undertakes.
Web
Now it's time to go to the site. For online playback, I have long since used a free flash player from uppod in minimalist settings that looks into the / flv stream and displays the track being played when playing. It looks like this:

And to display the current track, when the player is minimized or inactive, I, like many others confronted with this problem, until recently used a pad in the form of a .php script on the server that went to Icecast for statistics and returned a string with the name of the track being played. Itās time to get rid of intermediate steps, and I would like to show the covers on the site during online playback, since I now know how to give them away.
The problem is solved in two steps:
Add a custom header to the Nginx configuration for api that allows access to it via jQuery from another host:
add_header Access-Control-Allow-Origin *;
And put in the body of the web page of the radio station such a script:
var now_playing = ''; setInterval(function () { jQuery.ajax( { type: "GET", url: "http://api.<yoursite>/?partner_token=<token>&stream=/<stream>", dataType: "xml", success: xmlParser }) }, 5000); function xmlParser(xml) { var parsedXml = jQuery(xml); var title = parsedXml.find('title').text(); var artist = parsedXml.find('artist').text(); var arturl = parsedXml.find('arturl').text(); var song = artist.concat(" ā ").concat(title); if (now_playing !== song) { jQuery('div.now_playing').html(song); jQuery('div.cover_art').html(arturl); now_playing = song; } };
As you can see, once every five seconds, the script goes to the same place as the application, authorizes there, gets the .xml file and takes the played track and the link to the cover from it. And if they have changed since the last check, then they are written in the necessary divs of the radio stationās web page for display. Immediately I ask the gentlemen of the front-end developers not to swear at the possible clumsiness of the script - jQuery I see the first (well, well - the second), once in a lifetime. The script may be unsightly, but it works great.
Under the player, another div has been added in which the covers dynamically change.Conclusion
At this all the tasks are solved. Radio broadcasts as many years before, but now also displays the covers of the tracks being played and does it correctly. The little perfectionist inside my head is sleeping, snoring contentedly and not distracting from work.
I understand that the topic described is rather narrow-specific, and may be interesting to a small circle of people, but I think that my experience will still be useful to someone. So the full texts of all the above code, plus examples of Nginx settings and a description of the installation, are available on
GitHub .
All music!