As promised, I’ve written in under 100 lines of JavaScript (QtScript) a service for Amarok that plugs into ZX2C4 Music (ReadMe for ZX2C4 Music, Source for ZX2C4 Music). Obligatory screenshot:
Amarok Scriptable Service for ZX2C4 Music
The Amarok scripting API is very slick, especially the scripted services API.

It is not now configurable and is rather inefficient because getlisting.php does not support very complex queries (though I do take advantage of its sorting on lines 54-57), but this will be changed by the time I release an installable version of the script. Currently, it downloads a listing of the entire collection during the first request (lines 27-33), and then just reads from an in-memory database (multidimensional array) to query the different levels (lines 34-95). TMI: too much iteration.

Unfortunately there are some kinks, as Phonon often has trouble playing URLs and does not always show buffering information correctly (both gstreamer and xine backends). Look at lines 65 and below of getsong.php: this should be a working HTTP streaming implementation, but Phonon doesn’t dig it, for whatever reason. Any pointers here? The same issue crops up in the standalone qt player for ZX2C4 Music. Strange. Also, I use QTextDocument to convert HTML entities back to normal text (lines 14-22), but it’s very slow. Is there a better way to be doing this? Finally, callbackData (line 73) refuses to store an array, which means I have to use a nasty splitter; this is a bug presumably.

For the ZX2C4 Music hackers out there, here’s the source code to play with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
Importer.loadQtBinding("qt.core");
Importer.loadQtBinding("qt.network");
Importer.loadQtBinding("qt.gui");
 
function ZX2C4Music()
{
	ScriptableServiceScript.call(this, "ZX2C4 Music", 3, "Browse ZX2C4 Music", "The entire ZX2C4 collection, at your finger tips.", false);
}
var songs = null;
var delayedArgs = null;
function receiveDatabase(reply)
{
	songs = eval(reply)[1];
	var decoder = new QTextDocument();
	for (var i = 0; i < songs.length; i++)
	{
		for (var j = 0; j < songs[i].length; j++)
		{
			decoder.setHtml(songs[i][j]);
			songs[i][j] = decoder.toPlainText();
		}
	}
	onPopulate(delayedArgs[0], delayedArgs[1], delayedArgs[2]);
}
function onPopulate(level, callback, filter)
{
	if (songs == null)
	{
		delayedArgs = [level, callback, filter];
		new Downloader(new QUrl("http://music.zx2c4.com/getlisting.php?username=TOP&password=SECRET&language=javascript"), receiveDatabase);
		Amarok.Window.Statusbar.shortMessage("ZX2C4 Music collection is loading...");
		return;
	}
	if (level == 0)
	{
		var splitCallback = callback.split("&*/ZX2C4MUSICSPLITTER/*&");
		var notFirst = false;
		for (var i = 0; i < songs.length; i++)
		{
			if (songs[i][4] == splitCallback[0] && songs[i][3] == splitCallback[1])
			{
				notFirst = true;
				var item = Amarok.StreamItem;
				item.level = 0;
				item.itemName = songs[i][2];
				item.playableUrl = "http://music.zx2c4.com/getsong.php?username=TOP&password=SECRET&transcode=false&hash=" + songs[i][0];
				item.album = songs[i][3];
				item.artist = songs[i][4];
				item.track = songs[i][1];
				item.infoHtml = "";
				item.callbackData = "";
				script.insertItem(item);
			}
			else if (notFirst)
			{
				break;
			}
		}
	}
	else if (level == 1)
	{
		var lastAlbum;
		for (var i = 0; i < songs.length; i++)
		{
			if (callback == songs[i][4] && lastAlbum != songs[i][3])
			{
				lastAlbum = songs[i][3];
				var item = Amarok.StreamItem;
				item.level = 1;
				item.itemName = lastAlbum;
				item.playableUrl = "";
				item.infoHtml = "";
				item.callbackData = callback + "&*/ZX2C4MUSICSPLITTER/*&" + lastAlbum;
				script.insertItem(item);
			}
		}
	}
	else if (level == 2)
	{
		var lastArtist;
		for (var i = 0; i < songs.length; i++)
		{
			if (lastArtist != songs[i][4])
			{
				lastArtist = songs[i][4];
				var item = Amarok.StreamItem;
				item.level = 2;
				item.itemName = lastArtist;
				item.playableUrl = "";
				item.infoHtml = "";
				item.callbackData = lastArtist;
				script.insertItem(item);
			}
		}
	}
	script.donePopulating();
}
var script = new ZX2C4Music();
script.populate.connect(onPopulate);
April 12, 2009 · 1 comment


I just fixed four major blockers in ZX2C4 Music. I also moved the source into a public git.

Prior to the fix, only one php page could load at a time. When requesting a second php page while one was loading, the request would hang until the previous one had completed. This is not terribly problematic for ordinary web pages, but because I use php for file downloads, the entire web app would hang until the file had completed downloading. Bad news bears. After a lot of difficult debugging, tcpdumps, header inspects, ps aux, etc, I discovered the problem to be with session_start(). It turns out that only one session can be opened for writing at a time per session cookie, which means for a single user, php will only allow pages to write to his session one at a time. The solution was to call session_write_close() immediately after editing the last $_SESSION variable.

Some media players had trouble seeking in streams from the php file streamer because of bad http header range support. The sendFile() is now HTTP/1.1 compliant. Although, I’d like to add some caching support at some point.

Finally, and most notably, ZX2C4 Music no longer hangs when loading giant lists. It now loads 50 items at a time, and polls the location of the scrollbar every 100 milliseconds, and if it is close to the bottom, it requests the next 50 items. Polling is not ideal, but JavaScript does not have scroll events for divs. The pagination is implemented using MySQL’s LIMIT and OFFSET features. The display code took quite a bit of reworking and might still be a little buggy. Do try it out. Internet Explorer requires a special case because of its lack of support for writing to tables’ innerHTML, and Microsoft refuses to fix it. When adding an entire list to the download basket, it takes the time to download and display every item in the query, like the old behavior, and this may introduce some bugs. For the most part though, scrolling auto-pagination support is generally pretty slick.

The download basket/zip feature is no longer capped at 200 songs, as it no longer needs to call an external zip program and store the zip file temporarily. In pure php, I’ve written a custom on-the-fly zip file streamer, which works a lot faster.

As always, if you have suggestions, leave a comment below. If you’d like to download the latest HEAD of ZX2C4 Music, you can browse the repository here or download a tar of it here. Next on the list are HTML5 <audio> element support for web app streaming, Amarok plugin, update script, and maybe a revamped interface.

December 21, 2008 · 1 comment


Here’s a curious problem I’ve come across: There are N columns in a table of width W, with each column having content that requires a maximum width of M1, M2, M3, … , MN, where M1 + M2 + M3 + … + MN > W. Find an optimal size, O1, O2, O3, … , ON, for all columns in the table, where O1 + O2 + O3 + … + ON = W, and where for all i between 1 and N, Oi / W ≤ R, where R is an arbitrary ratio less than 1 and greater than 0. That is to say, optimally sized means each Oi / W = min(R, Mi / (M1 + M2 + M3 + … + MN)), but when the min function chooses its first argument, the extra space, ([second argument] – [first argument]) * W, needs to be proportionally distributed to the remaining Oj, where j is between 1 and N and does not equal i, and where “proportionally” means related to the proportions of Mj / (M1 + M2 + M3 + … + MN). However, proportionally distributing this excess to the remaining columns may push a remaining column’s ratio over R, which means some of that excess might further have to be redistributed.

The iterative approach to this problem is easy, but I am interested in determining a non-iterative solution to finding all of the Oi, where I simply have a formula Oi = [yada yada yada].

Here’s code for the iterative solution when applied to columns of a QTreeView, with N = 3 and R = .42, which is derived from my AutoSizingList class of ZMusicPlayer. It uses QTreeView’s sizeHintForColumn to determine Mi.

So essentially, the problem is that the do-while on lines 18 and 41 should be eliminated. It is required in the first place because when proportionally redistributing the difference between the arguments of the min function (outlined in the first paragraph), the ratios of the columns receiving this extra width may exceed R, which means another iteration must be done to readjust. Anyone have a non-iterative solution? Also, to the Qt experts out there, why is the hack required on line 49?

I am interested both in a practical Qt approach and a mathematical approach, the latter being more interesting. Doesn’t this seem like functionality that aught to already be a part of Qt?

Update: A conversation with a friend over AIM has produced a plain-English explanation in the form of a dialogue of the above problem.

Jason: so you’re drawing a table
Jason: on a piece of paper
Jason: making all the lines and rows
Jason: so this table happens to have 3 columns
Jason: and a bunch of rows
Jason: make sense?
Pasternak: yes
Jason: ok
Jason: now each cell of the table is supposed to have some information in it
Jason: different length information takes up more width in the table
Jason: Rob Pasternak takes up more room than Yi Chan, for example
Jason: it’s wider
Jason: it has more characters
Jason: right?
Pasternak: yes
Jason: now say that the biggest piece of data in the first column requires a width M1, the biggest in the second column requires a width M2, and in the third, M3
Jason: Rob Pasternak, Yi Chan, and Salvadora Manello Envelopa Dali
Jason: are the biggest pieces of data for each of the three columns respectively
Pasternak: ok
Jason: now let’s say you’re writing down your table on a napkin
Jason: so you can only make it a certain width
Jason: because the napkin is small
Jason: and you’re bad at writing with tiny handwriting
Pasternak: ok
Jason: so you think to yourself “well, i will just make each column have its width be in the same proportion to the tiny width of the entire table as if i had tons of room to draw the table and was able to make each column have the width equal to the column’s biggest name”
Jason: make sense?
Pasternak: ok
Pasternak: yes
Jason: so a simple equation for finding how big each column would be would just to do this
Jason: column1 = ( M1 / (M1 + M2 + M3) ) * tinyNapkinWidth
Jason: right?
Jason: and similarly for columns 2 and 3
Pasternak: yeah
Jason: ok so lets say you do that and then you notice that
Jason: Salvadora Manello Envelopa Dali is taking up way too much space on the table
Jason: so much so that the other two columns are too tiny to show any relevant information
Pasternak: but i thought you said
Pasternak: because each of those were the maximum
Pasternak: oh wait
Pasternak: i gotcha
Pasternak: cause the napkin is too small to make Chan visible at the right proportions
Jason: yeah
Jason: even though it’s all perfectly proportioned,
Pasternak: yeah
Jason: Salvadora Manello Envelopa Dali is just so big that
Jason: he makes Chan tiny
Jason: so you think to yourself
Jason: “how about i impose a maximum ratio each column may take up. I hereby declare that no column may use more than 40% of the entire width!”
Jason: so, you apply the first process of column1 = ( M1 / (M1 + M2 + M3) ) * tinyNapkinWidth etc
Jason: but then you say something like
Jason: “if column1 is greater than 40% of tinyNapkinWidth, make column1 equal to 40% of tinyNapkinWidth”
Jason: with me?
Pasternak: yes
Jason: so then what are you going to do with that excess width you cut off of column1?
Jason: it has to go somewhere
Jason: since we want to use all of the napkin width
Jason: so you distribute it to the other two columns proportionally
Jason: not half to one and half to the other, but in a proportion equal to the amount of data they contain
Jason: seem reasonable?
Pasternak: yeah but then one of them could go over 40
Jason: exactly!
Jason: that’s the problem
Jason: so then you do the process all over again
Jason: asking “does any column exceed 40%?”
Jason: over and over until it’s perfect
Jason: this is the iterative method
Jason: i want to figure out an expression of column1,2,3 that is as simple as our original one of “column1 = ( M1 / (M1 + M2 + M3) ) * tinyNapkinWidth” but that takes into consideration our 40% restriction
Jason: and doesn’t rely on iteration
Pasternak: for any number of columns i assume
Jason: yeah
Jason: right
November 29, 2008 · 21 comments


This post is a follow up to Introducing ZX2C4 Music.

Update: source code available here.

I have just finished generalizing aspects of ZX2C4 Music so that it’s installable on a variety of different servers. I have no experience with releasing PHP apps to the general public, so I’d appreciate some feedback on the installation process. You can download the tarball here. Be sure to read README.txt.

November 27, 2008 · 1 comment


Update: source code available here.

A few months ago I switched website hosts from Netfirms to Site5, because Site5 had a good deal for a plan with unlimited bandwidth and disk space. The first thing I did was upload my entire music collection.

To access all of my music remotely via any internet-connected web browser, I wrote ZX2C4 Music, which is password protected to avoid the law, but if you’d like to view the interface just for curiosity, ask me in the comments section, and I’ll e-mail you a password. It uses PHP/MySQL for managing the music, hashing each file with an sha1, TagLib for reading tags, and JavaScript/HTML as an interface:

Search queries are done with AJAX, and playing is done with Flash, if available; otherwise it defaults to browser plug-ins. Soon I should add HTML5′s <audio> element support. Since Flash only supports MP3, the php backend optionally transcodes non-mp3 songs on the fly by using ffmpeg. Songs can be downloaded in zipped bundles by using the download basket feature. The web interface still needs some work, most notably scrolling-pagination for the song table, but it’s definitely usable. Many have pointed my attention to Ampache, but I found it too big and overwhelming. You can view the source code here.

Of course, the next step was writing a Qt or KDE application that interfaces directly with the web backend using Phonon. Meet ZMusicPlayer:

It downloads and displays a compressed and sorted xml listing of all the songs and their sha1s. The columns of the list automatically size optimally using an awesome algorithm that still needs some tweaking (why is subtracting 40 required on line 87?). Suggestions?

It plays the songs by passing the appropriate QUrl to Phonon, and this is where the trouble begins. The xine backend on KDE 4.1.2 just skips one song to another most of the time, and when it does miraculously play, seeking (via HTTP partial requests) is unavailable. The headers for each of the server responses for song file requests provide the appropriate parameters for HTTP seeking. Maybe this has already been fixed in trunk, but if not, I aught to investigate the problem as soon as I switch my 4.1.2 desktop completely to trunk. GStreamer with Qt 4.4 allows for proper streaming if a GStreamer HTTP plug-in is installed (gst-plugins-good includes a wrapper plugin for libsoup, which works, for those who don’t want to install the gnome vfs plugin), which is understandable. However, seeking only works for mp3 files; m4a files play but cannot seek, even though the pertinent response headers are the same for both file types. On the Windows backend, the seeking situation is hopeless, and on the OS X backend, songs do not play at all. Also, xine is the only one that emits the proper buffering signals. On some setups though, some of these problems go away. I still need to do some more thorough debugging.

If you’d like to download and build ZMusicPlayer, you can browse the source, clone the repository with git clone http://git.zx2c4.com/zmusicplayer.git, or download a tarball. Depending on your package setup, you may have to tinker with the includes for the phonon headers. I’d love to hear some suggestions, so leave comments below. Once I stabilize the Qt code, I’d like to develop some optional KDE extensions, particularly integration with the KDE’s keyboard shortcuts.

Also: I’ve switched from blogging on kdedevelopers.org to my own WordPress blog so that anyone can post comments without requiring an approved account. So, comment away. If you wanted to comment on my first post, but were unable to, you can now comment here. To comment on this post, use the link below.

Update Oct 25, 3:31pm: Several of you have e-mailed me to say that the browser hangs when loading a unfiltered giant collection. This is indeed the case, as no current browser is able to write 10,000 rows to a table in a reasonable amount of time. This is why above I speak of “scrolling-pagination”, which means that the table would load 100 or so rows at a time, and then auto-populate the remaining rows in 100 row increments every time the user scrolled to near the end.

October 24, 2008 · 24 comments