Get Microsoft Silverlight

This blog — and all projects contained within — was made with free/demo software available through the Microsoft Web Platform. Go check it out today, it’s pretty cool.

Get the Microsoft Web Platform.

Django on a Windows Server with IIS

Part of the terms of this project were that we deploy and use a Windows server running IIS for both the blog, and for any other subsite, such as rsstunes.com.

As I’ve mentioned in previous posts, I was pleasantly surprised to see just how easy the Microsoft Web Installer made getting PHP running on Windows. If you’ve got to do it, there’s no better way.

A vast majority of our internally development over the last year has been in PHP. It’s an agile, relatively mature language, and it runs just about anywhere a client could ask it to.

But as we’ve matured as a development team, I’ve been pushing us increasingly towards using Python, and an excellent web framework called Django.

Django, and Python, are most assuredly not supported by the Web Installer, and while I knew I might be shooting myself in the foot, I decided that, for the second version of the rsstunes.com site, we’d use Django, and I’d sort out how to get it running on IIS.

Since the vast majority of Django developers are running a standard Linux stack, there are few, if any good resources out there on getting Django running. There are a few hints, and assorted messages strewn about mailing lists, but nothing authoritative.

After about two weeks of configuration headaches, and crashes, I’ve managed to get a stable, production ready installation of Django running.

Here’s how.

Python.

Before you get started, you need to get a copy of Python running on your Windows server. The easiest way is to download and install the Windows Python 2.6.4 installer from Python.org.

I installed mine at the root of the C: drive under a folder called “python”.

MySQL & Python MySQLdb

For our projects, we develop using MySQL as our database server. If you’re using Postgres, you’re on your own getting everything running.

First, You’ll need to download and install MySQL. There are MSI packages available from MySQL.com. I recommend using MySQL 5.1.

I’d also recommend snagging the MySQL GUI tools. It makes administering the server on Windows infinitely easier.

The final hurdle to a working Django base is to install the Python bindings for MySQL, which, at least on my Snow Leopard installation, can be a real pain. Lucky for us, CodeGood maintains a binary installer for the MySQLDb Python module for Windows. Download and install it.

Optionally, you may want to install the Python Imaging Module, or PIL. Trust me, doing it now will save you some time.

Testing Django

Download the most recent release of Django and install it into C:\python\Lib\site-packages.

Follow the Django Getting Started Tutorial and get a very basic Django project running, just to verify your Python installation is in working order. Chances are, it will be.

IIS & Python

As best I can tell, the standard operating procedure for running Django on a Windows Server is to install Apache and run it under mod_python or mod_wsgi. This seemed fantastically silly to me.

First, Apache, while it runs under Windows just fine, always feels like a kludge when running under Windows. All of the UNIX conventions just stick out like a sore thumb. Seeing that IIS is so stable and awesome at running PHP, why wouldn’t it work to run Django?

The best option seems to be PyISAPIe, an ISAPI plugin for IIS that allows Python scripts to serve web pages. I tried a few other options, such as isapi_wsgi, and just couldn’t keep the server running for longer than one or two requests without a crash.

Download and install 1.1.0-rc4 of PyISAPI and install it. I’d recommend installing it at the root of the C: drive, just like we did for Python.

Your Site Structure

Each Django site will essentially need it’s own copy of the pyisapie.dll file, and it’s own configuration files.

The directory structure I ended up going with broke down to two folders.

Under my inetpub directory, I created a directory called “sites”, which will store each of my Django-powered sites. Each project has it’s own directory under that, and within each of those directories I have a folder for pyisapie and another for my Django project.

In the case of rsstunes.com the structure looks like this:

IIS Directory

The “log” and “public” folders aren’t strictly necessary.

Inside of pyisapie, I have a copy of the DLL file, and the configuration files. Here’s a ZIP archive of pyisapie folder, which you can use to get started.

Next, place your Django project in the python subdirectory.

Configure IIS

We need to register PyISAPIe as being a valid server extension in IIS, and it needs to be setup very specifically if you want to run multiple Django-powered websites.

First, we’ll need to register our site-specific copy of the pyisapie.dll file. Open your IIS Manager, and click on “Web Service Extensions”. Next, click on “Add a new service extension”. Give it a name you’ll recognize (I went with “rsstunes python”), and then click “Add”. Find your site folder, and under pyisapie, click to select the pyisapie.dll file.

Click the checkbox next to “Set extension to Allowed”.

Configure an Application Pool

Due to how PyISAPIe works, you’ll want a separate Application Pool for each of your Django-powered sites. Performance wise, this isn’t great if you’re planning on running dozens of Django sites on one server, but it works great for a handful in my testing.

In your IIS Manager, right-click on Application Pools, and create a new application pool. Again, name it something that will make sense to you.

In terms of settings, I’m still tweaking. For load testing, I use a tool called siege, and check to make sure the server can handle a minimum of 100 requests per second. To achieve this, I set the application pool to recycle workers every 60 minutes, and after 250 requests. You can play with the settings and see what works for you.

Add the Site

Add a site as you would normally under IIS. I set my home directory to that blank “public” directory you saw earlier. It doesn’t actually matter since we’re going to point all requests at our PyISAPIe file.

Under Home Directory, give your application a name, and click “Configuration”. Under Wildcard application maps, click Insert, and browse to your site-specific PyISAPIe.dll file. Make sure “Check that file exists” is unchecked.

Since serving static files with Python is pointless, I’d also go ahead and add a few Virtual Directories for your static files (generally stored in your Django project’s “static” directory), and, if you’re using the Django admin, one for “media”, pointed at C:\python\Lib\site-packages\django\contrib\admin\media.

Lastly, make sure you set the site’s application pool to the one you created earlier.

Edit Isapi.py

The file Isapi.py under “pyisapie/Http” is the actual adapter file PyISAPIe looks for and uses to serve up a request.

You’ll want to do two things.

First, append the full path to your Django project to the system PYTHON_PATH using sys.append, as I’ve done in the example.

Last, you’ll want to set os.environ['DJANGO_SETTINGS_MODULE'] to your projects settings package.

Start it up

Once you’ve done all that, stop the Application Pool you created and the site. Start the application pool, and then the site. And yes, the order matters.

Visit your site’s URL in a web browser, and you should see your site up and running.

Nice Job NoPattern!

Windows 7 launched last week. (Unofficially, it’s been working great for us here even though it’s technically unsupported by both Bootcamp and VMware Fusion 2.) We were super stoked to see that the shipping retail release came stock with a shiny desktop designed by Chuck Anderson/No Pattern. Pretty slick work, and kudos to Chuck– that’s a nice piece to have in the book!

Lesson, the first.

I’ve actually built rssTunes three times, and we’re about to embark on a fourth build.

Each time I’ve completely scrapped most of my code, not just out of some perfectionist’s stupidity, though I’m sure that played into it, but more because after each build was complete, I realized I’d approached the entire project incorrectly.

Each build failed for very specific reasons. Today, I’m gonna focus on just one of them.

Security Sandboxes

My first build of the player was strictly a proof-of-concept.

Whenever I start a project, one of the first things I do is sort out exactly how I’m gonna accomplish specific features. If I’m dealing with a new language, or platform – or in this case both – I’ll try to cobble together some very basic proofs that show my basic approach, and how much time I think it’ll take me, isn’t completely off.

rssTunes isn’t a very complicated application, at least not compared to other things. It downloads a list of MP3 files from a server, and plays them in linear order. It shows you which song is currently playing, and who the song was recorded by. This is not rocket science.

In my first build, I wired up a single MediaElement control, added a pause and play button, and some text fields to show the song and artist name. I threw an example file up on our server, and plugged the excellent TagLibSharp into place. Everything seemed to work just dandy, so I called it a day.

A few days later, I wanted to battle-test the ability to download a list of tracks. I wrote a quick RSS parser using SimplePie, that spit out a JSON-encoded array of tracks, and then wired up a simple class inside the application that would keep track of a list of files and let the user advance trough them, swapping out MediaElement’s DataContext as the user progressed through the list of files.

The music played. The song and artist name did not update. In fact, instead of working, it threw a rather heinous security exception and then the application crashed. After pulling my hair out, cursing the gods, and swearing off C#, I went back and started pulling everything apart. I swapped out my new track-listing class for my original one, which listed only the single file on my server. Everything worked again. I swapped in a single track from my list of downloaded tracks, and the application went boom. I paced, and smoked half-a-pack of cigarettes. I cursed the gods again. I started to become convinced that our idea was impossible, or at least not possible within our deadline, and I’d have to write an entire MP3 decoder on my own in C#. I get a little over-dramatic sometimes.

And then I remembered sandboxing.

An explanation.

Silverlight, like any internet plugin, is really rather dangerous. It presents a complete application environment where a developer can do any number of utterly awesome things, and can do them without any user interaction other than opening a web page.

One of the most basic really-dangerous things it can do is talk to a web server. I realize that doesn’t sound all that dangerous, but hang on, it gets better. Silverlight talks to web servers through your web browser. Queue scary music.

Every time your web browser makes a call to a web server – or opens a web page in laymen’s terms – it does more than just say, “Hey, give me a web page.” It also sends with it a whole slew of interesting tidbits about you, the user. These tidbits are usually stored in cookies, and every cookie your web browser has stored from a specific web server is sent over every time it makes any request to that web server.

That’s how things like “logging into” a web site work. You “log in”, and the web server sends down a cookie to your web browser with some identifying bit of information. Next time you try to open any other web page from that same web site, your web browser sends that cookie along.

Since Silverlight is asking your web browser to make requests to web servers for it, any request is going to have all of those delicious cookies attached to it.

Which means, from my Silverlight application, I could, for instance, start making simple requests to every known bank web site, searching for one in which you are actively logged into, and then wire a whole bunch of your money to myself. Because, for all your bank’s web server knowledge, my Silverlight application, which is running in your web browser, is doing things on your behalf.

Clearly, no one in their right mind would run Silverlight, or Flash, or anything else for that matter, if this were possible (though it’s completely possible to do this using just HTML and JavaScript right now using something called cross-site scripting hacks, but that’s completely outside the jurisdiction of this post).

To make Silverlight more secure, Microsoft prevents my application from making arbitrary requests to any web server at all. It “sandboxes” my application, and before it allows any request to be made, it checks with each web server and asks the web server whether or not the application is allowed to make the request.

It does this by checking for a file on the web server called either cross_domain.xml, the same file Flash uses to solve this problem, or clientaccesspolicy.xml, which is specific to Silverlight. If one of those files doesn’t exist on the web server, Silverlight won’t let you make a request to that web server.

But there are exceptions.

For instance, in my first build, the music played just fine no matter what server I was requesting the music from. Thankfully, Microsoft added an exemption to this rule whenever Silverlight can prevent me, the developer, from accessing the actual data contained in whatever file I’m requesting. Since MediaElement handles decoding the MP3 file and playing it for me, I have no need to access the actually data within the MP3 file. So that works just fine.

But to figure out what the title of the song playing is, and who it’s by, I have to read parts of the file looking for ID3 tags. MediaElement provides no access to this information on it’s own (hey, Microsoft, fix that!) so I have to read in the MP3 file and search for the tags on my lonesome.

Obviously, this is impossible given the sandboxing.

I had to rethink my whole approach.

The fix.

Now, the generally fix for this problem is to build a proxy server. Basically, you build a script on your server that will request files and web pages for you and pass back the results.

This is a generally terrible idea to me.

First, in our case, we’re not talking about passing through small requests. We’re talking about passing back potentially gigantic MP3 files. Having my server come between you and the MP3 file could cause massive delays, and completely ruin your listening experience.

So proxying everything wasn’t an option.

However, I’d already realized that building a full-fledge RSS parser in Silverlight would be a waste of time. The security sandbox would get in my way, not to mention the massive delays in start-up time and refreshing your feeds. I was originally going to build a simple proxying RSS reader, that would take in a single RSS feed URL and return a list of posts for Silverlight to find MP3 files from.

So why not just have the server do all the work?

If the rssTunes application become a thin-client to a robust web service, all of my problems would be solved. Huzzah!

Making the client.

The next proof-of-concept scrapped all of the ID3 tag reading junk, all of the RSS reading logic, and instead assumed it would be making a call to the same web server it was hosted on, and requesting a list of not just MP3 file URLs, but a full record for each file, including all know ID3 tags.

By pulling all of the parse-and-fetch logic out of the Silverlight application, I could focus on making it as fast and responsive as possible, and would have more time to add bells and whistles later.

I wrote a small army of PHP scripts that live on the Mess with Silverlight web server, that maintained a list a feeds for each account, and periodically fetched new items, looked for MP3 files within them, and then parsed each files ID3 tags.

While it’s not strictly related to Silverlight development, I had a few problems parsing ID3 tags as well.

Quick follow-up.

If you were using the first public build of the application, you may remember that many of the songs would show “Unknown Artist” or “Unknown Track”.

The problem was simple. I didn’t want to download every MP3 file in it’s entirety. It would waste our bandwidth, and it would take too long to refresh feeds for our users.

ID3 tags are stored at the beginning of an MP3 file. Their stored there specifically to ensure that, if you’re streaming an MP3 file off of a web server, you don’t have to wait for the entire file to download before seeing any information about the track.

The problem with every ID3 parsing script I could find, PHP or otherwise, is that they assume you’ve downloaded the entire file. I’d managed to “cheat”, but only downloading the first 16k of each MP3 file into a temporary file, and sending that portion of the file of the parsers. For most files, this worked just fine. However, for some, it failed miserably.

The only solution was to build my own ID3 tag parser that understood it was streaming the file from a web server, and would only download information as it needed it.

Based loosely on this class from phpClasses.org, the following PHP class takes in a URL, and downloads only the exact amount of the file necessary to parse the ID3 tags.

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
<?php
 
define('HEADER_LENGTH', 10);
define('HEADER_LENGTH_V', 6);
define('CHUNK_SIZE', 1024);
 
class mp3id3
{
 
	public $has_error;
	public $frames;
 
	private $path;
	private $read_so_far;
	private $id3_version;
	private $id3_header;
	private $tag_size;
	private $id3_header_data;	
	private $fh;
	private $data_so_far;
	private $flags;
	private $full_data;
	private $temporary_file;
 
	var $v2_frame_descriptions = array(
		'CNT' => 'Play counter',
		'COM' => 'Comments',
		'CRA' => 'Audio encryption',
		'CRM' => 'Encrypted meta frame',
		'ETC' => 'Event timing codes',
		'EQU' => 'Equalization',
		'GEO' => 'General encapsulated object',
		'IPL' => 'Involved people list',
		'LNK' => 'Linked information',
		'MCI' => 'Music CD Identifier',
		'MLL' => 'MPEG location lookup table',
		'PIC' => 'Attached picture',
		'POP' => 'Popularimeter',
		'REV' => 'Reverb',
		'RVA' => 'Relative volume adjustment',
		'SLT' => 'Synchronized lyric/text',
		'STC' => 'Synced tempo codes',
		'TAL' => 'Album/Movie/Show title',
		'TBP' => 'BPM (Beats Per Minute)',
		'TCM' => 'Composer',
		'TCO' => 'Content type',
		'TCR' => 'Copyright message',
		'TDA' => 'Date',
		'TDY' => 'Playlist delay',
		'TEN' => 'Encoded by',
		'TFT' => 'File type',
		'TIM' => 'Time',
		'TKE' => 'Initial key',
		'TLA' => 'Language(s)',
		'TLE' => 'Length',
		'TMT' => 'Media type',
		'TOA' => 'Original artist(s)/performer(s)',
		'TOF' => 'Original filename',
		'TOL' => 'Original Lyricist(s)/text writer(s)',
		'TOR' => 'Original release year',
		'TOT' => 'Original album/Movie/Show title',
		'TP1' => 'Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group',
		'TP2' => 'Band/Orchestra/Accompaniment',
		'TP3' => 'Conductor/Performer refinement',
		'TP4' => 'Interpreted, remixed, or otherwise modified by',
		'TPA' => 'Part of a set',
		'TPB' => 'Publisher',
		'TRC' => 'ISRC (International Standard Recording Code)',
		'TRD' => 'Recording dates',
		'TRK' => 'Track number/Position in set',
		'TSI' => 'Size',
		'TSS' => 'Software/hardware and settings used for encoding',
		'TT1' => 'Content group description',
		'TT2' => 'Title/Songname/Content description',
		'TT3' => 'Subtitle/Description refinement',
		'TXT' => 'Lyricist/text writer',
		'TXX' => 'User defined text information frame',
		'TYE' => 'Year',
		'UFI' => 'Unique file identifier',
		'ULT' => 'Unsychronized lyric/text transcription',
		'WAF' => 'Official audio file webpage',
		'WAR' => 'Official artist/performer webpage',
		'WAS' => 'Official audio source webpage',
		'WCM' => 'Commercial information',
		'WCP' => 'Copyright/Legal information',
		'WPB' => 'Publishers official webpage',
		'WXX' => 'User defined URL link frame'		
	);
 
	var $v3_frame_descriptions = array(
			'AENC' => 'Audio encryption',
			'APIC' => 'Attached picture',
			'COMM' => 'Comments',
			'COMR' => 'Commercial frame',
			'ENCR' => 'Encryption method registration',
			'EQUA' => 'Equalization',
			'ETCO' => 'Event timing codes',
			'GEOB' => 'General encapsulated object',
			'GRID' => 'Group identification registration',
			'IPLS' => 'Involved people list',
			'LINK' => 'Linked information',
			'MCDI' => 'Music CD identifier',
			'MLLT' => 'MPEG location lookup table',
			'OWNE' => 'Ownership frame',
			'PRIV' => 'Private frame',
			'PCNT' => 'Play counter',
			'POPM' => 'Popularimeter',
			'POSS' => 'Position synchronization frame',
			'RBUF' => 'Recommended buffer size',
			'RVAD' => 'Relative volume adjustment',
			'RVRB' => 'Reverb',
			'SYLT' => 'Synchronized lyric/text',
			'SYTC' => 'Synchronized tempo codes',
			'TALB' => 'Album/Movie/Show title',
			'TBPM' => 'BPM (beats per minute)',
			'TCOM' => 'Composer',
			'TCON' => 'Content type',
			'TCOP' => 'Copyright message',
			'TDAT' => 'Date',
			'TDLY' => 'Playlist delay',
			'TENC' => 'Encoded by',
			'TEXT' => 'Lyricist/Text writer',
			'TFLT' => 'File type',
			'TIME' => 'Time',
			'TIT1' => 'Content group description',
			'TIT2' => 'Title/songname/content description',
			'TIT3' => 'Subtitle/Description refinement',
			'TKEY' => 'Initial key',
			'TLAN' => 'Language(s)',
			'TLEN' => 'Length',
			'TMED' => 'Media type',
			'TOAL' => 'Original album/movie/show title',
			'TOFN' => 'Original filename',
			'TOLY' => 'Original lyricist(s)/text writer(s)',
			'TOPE' => 'Original artist(s)/performer(s)',
			'TORY' => 'Original release year',
			'TOWN' => 'File owner/licensee',
			'TPE1' => 'Lead performer(s)/Soloist(s)',
			'TPE2' => 'Band/orchestra/accompaniment',
			'TPE3' => 'Conductor/performer refinement',
			'TPE4' => 'Interpreted, remixed, or otherwise modified by',
			'TPOS' => 'Part of a set',
			'TPUB' => 'Publisher',
			'TRCK' => 'Track number/Position in set',
			'TRDA' => 'Recording dates',
			'TRSN' => 'Internet radio station name',
			'TRSO' => 'Internet radio station owner',
			'TSIZ' => 'Size',
			'TSRC' => 'ISRC (international standard recording code)',
			'TSSE' => 'Software/Hardware and settings used for encoding',
			'TYER' => 'Year',
			'UFID' => 'Unique file identifier',
			'USER' => 'Terms of use',
			'USLT' => 'Unsychronized lyric/text transcription',
			'WCOM' => 'Commercial information',
			'WCOP' => 'Copyright/Legal information',
			'WOAF' => 'Official audio file webpage',
			'WOAR' => 'Official artist/performer webpage',
			'WOAS' => 'Official audio source webpage',
			'WORS' => 'Official internet radio station homepage',
			'WPAY' => 'Payment',
			'WPUB' => 'Publishers official webpage'
		);	
 
// Methods
 
	public function __construct($path, $delayed_parse = false)
	{
		$this->frames = array();		
		$this->path = $path;
		$this->fh = @fopen($this->path, "rb");		
		if (!$this->fh) $this->has_error = true;
		if (!$delayed_parse) $this->parse();
	}
 
	public function __destruct()
	{
		if ($this->fh) @fclose($this->fh);
	}	
 
	public function parse()
	{
		// Ensure file has a header
		if (!$this->get_size_of_tags()) $this->has_error = true;
		if (!$this->read_tags()) $this->has_error = true;
	}
 
	public function get_size_of_tags()
	{
		if ($this->fh)
		{
			$this->id3_header_data = fread($this->fh, 10);
			$this->id3_header = @unpack('a3identifier/Cversion/Crevision/Cflag/Csize0/Csize1/Csize2/Csize3', $this->id3_header_data);		
			if(!$this->id3_header || $this->id3_header['identifier'] != 'ID3') return false;
 
			$this->tag_size = ($this->id3_header['size0'] & 0x7F) << 21 | ($this->id3_header['size1'] & 0x7F) << 14 | ($this->id3_header['size2'] & 0x7F) << 7 | ($this->id3_header['size3']);
 
			if(($this->tag_size = intval($this->tag_size)) < 1) return false;
			else return $this->tag_size;
		}
	}
 
	public function read_tags()
	{
		if ($this->id3_header['version'] == 2) $this->read_v2_tags();
		if ($this->id3_header['version'] > 2) $this->read_v4_tags();
		return true;
	}
 
	public function artist()
	{
		if (($this->id3_header['version'] == 2) && isset($this->frames['TP1'])) return $this->frames['TP1']->data;
		if (($this->id3_header['version'] > 2) && isset($this->frames['TPE1'])) return $this->frames['TPE1']->data;	
		return "Unknown Artist";
	}
 
	public function song()
	{
		if (($this->id3_header['version'] == 2) && isset($this->frames['TT2'])) return $this->frames['TT2']->data;
		if (($this->id3_header['version'] > 2) && isset($this->frames['TIT2'])) return $this->frames['TIT2']->data;	
		return "Unknown Track";
	}	
 
	public function read_v2_tags()
	{
		$temporary_file = tmpfile();
		for($read = 0; $read < $this->tag_size; $read += CHUNK_SIZE)
		{
			fwrite($temporary_file, fread($this->fh, CHUNK_SIZE));
		}
		fseek($temporary_file, 0);
 
		while(1)
		{
 
			if(ftell($temporary_file) >= $this->tag_size) break;
 
			$frame = new StdClass();
 
			$header_chunck = fread($temporary_file, HEADER_LENGTH_V);
			$frame->header = @unpack('a3frameid/Csize0/Csize1/Csize2', $header_chunck);
			if(!$frame->header || !$frame->header['frameid']) continue;
 
			$frame->id = $frame->header['frameid'];
			$frame->description = 'Unknown';
			$frame->header['size'] = $frame->header['size0'] << 16 | $frame->header['size1'] << 8 | $frame->header['size2'];
			if(($frame->size = $frame->header['size']) < 1 || (ftell($temporary_file) + $frame->size) > $this->tag_size) continue;
 
			if(isset($this->v2_frame_descriptions[$frame->id])) 
			{
				$frame->description = $this->id3v2_frame_descriptions[$frame->id];
			} 
			else 
			{
				switch(strtoupper($frameid{0}))
				{
					case 'T': $frame->description = 'Text'; break;
					case 'W': $frame->description = 'URL'; break;
				}
			}
 
			$frame->charsetdata = @unpack('ctype', fread($temporary_file, 1));
			$frame->charset = null;
 
			switch (intval($frame->charsetdata['type'])) 
			{
				case 0: $frame->charset = 'ISO-8859-1'; break;
				case 1: $frame->charset = 'UTF-16'; break;
				case 2: $frame->charset = 'UTF-16BE'; break;
				case 3: $frame->charset = 'UTF-8'; break;
			}
 
			if ($frame->charset) 
			{
				$frame->datasize = $frame->size - 1;
			}
			else 
			{
				$frame->datasize = $frame->size;
				fseek($temporary_file, ftell($temporary_file) - 1);
			}			
 
			$frame->data = @unpack("a{$frame->datasize}data", fread($temporary_file, $frame->datasize));
			$frame->data = (intval($frame->charsetdata['type']) > 0) ? utf8_decode($this->utf16_to_utf8($frame->data['data'])) : $frame->data['data'];
 
			if ($frame->id == 'COMM') 
			{
				$frame->lang = substr($frame->data, 0, 3);
				$frame->data = substr($frame->data, 3 + ($frame->data{3} == "\x00" ? 1 : 0));
			} else {
				$frame->lang = '';
			}
 
			$this->frames[$frame->id] = $frame;
			if (isset($this->frames['TP1']) && isset($this->frames['TT2'])) break;
		}
		fclose($temporary_file);
	}
 
	public function read_v4_tags()
	{
		$temporary_file = tmpfile();
		for($read = 0; $read < $this->tag_size; $read += CHUNK_SIZE)
		{
			fwrite($temporary_file, fread($this->fh, CHUNK_SIZE));
		}
		fseek($temporary_file, 0);
 
		while(1)
		{
 
			if(ftell($temporary_file) >= $this->tag_size) break;
 
			$frame = new StdClass();
 
			$header_chunck = fread($temporary_file, HEADER_LENGTH);
			$frame->header = @unpack('a4frameid/Nsize/Cflag0/Cflag1', $header_chunck);	
			if(!$frame->header || !$frame->header['frameid']) continue;
 
			$frame->id = $frame->header['frameid'];
			$frame->description = 'Unknown';
 
			if(($frame->size = $frame->header['size']) < 1 || (ftell($temporary_file) + $frame->size) > $this->tag_size) continue;
 
			if(isset($this->v3_frame_descriptions[$frame->id])) 
			{
				$frame->description = $this->id3v2_frame_descriptions[$frame->id];
			} 
			else 
			{
				switch(strtoupper($frameid{0}))
				{
					case 'T': $frame->description = 'Text'; break;
					case 'W': $frame->description = 'URL'; break;
				}
			}
 
			$frame->flag = array($this->convert_flag($frame->header['flag0']), $this->convert_flag($frame->header['flag1']));
 
			$frame->charsetdata = @unpack('ctype', fread($temporary_file, 1));
			$frame->charset = null;
 
			switch (intval($frame->charsetdata['type'])) 
			{
				case 0: $frame->charset = 'ISO-8859-1'; break;
				case 1: $frame->charset = 'UTF-16'; break;
				case 2: $frame->charset = 'UTF-16BE'; break;
				case 3: $frame->charset = 'UTF-8'; break;
			}
 
			if ($frame->charset) 
			{
				$frame->datasize = $frame->size - 1;
			}
			else 
			{
				$frame->datasize = $frame->size;
				fseek($temporary_file, ftell($temporary_file) - 1);
			}			
 
			$frame->data = @unpack("a{$frame->datasize}data", fread($temporary_file, $frame->datasize));
			$frame->data = (intval($frame->charsetdata['type']) > 0) ? utf8_decode($this->utf16_to_utf8($frame->data['data'])) : $frame->data['data'];
 
			if ($frame->id == 'COMM') 
			{
				$frame->lang = substr($frame->data, 0, 3);
				$frame->data = substr($frame->data, 3 + ($frame->data{3} == "\x00" ? 1 : 0));
			} else {
				$frame->lang = '';
			}
 
			$this->frames[$frame->id] = $frame;
			if (isset($this->frames['TPE1']) && isset($this->frames['TIT2'])) break;			
		}
		fclose($temporary_file);
	}
 
	private function get_frame($name)
	{
		if (isset($this->frames[$name])) if (isset($this->frames[$name]->data)) return $this->frames[$name]->data;
		else return "Unknown";
	}
 
	private function convert_flag($flag, $convtobin = true, $length = 8) 
	{
			$flag = $convtobin ? decbin($flag) : $flag;
			$recruit = $length - strlen($flag);
			if($recruit < 1) return $flag;
			return sprintf('%0'.$length.'d', $flag);
	}
 
	private function utf16_to_utf8($str) 
	{
	    $c0 = ord($str[0]);
	    $c1 = ord($str[1]);
 
	    if ($c0 == 0xFE && $c1 == 0xFF) {
	        $be = true;
	    } else if ($c0 == 0xFF && $c1 == 0xFE) {
	        $be = false;
	    } else {
	        return $str;
	    }
 
	    $str = substr($str, 2);
	    $len = strlen($str);
	    $dec = '';
	    for ($i = 0; $i < $len; $i += 2) {
	        $c = ($be) ? ord($str[$i]) << 8 | ord($str[$i + 1]) : 
	                ord($str[$i + 1]) << 8 | ord($str[$i]);
	        if ($c >= 0x0001 && $c <= 0x007F) {
	            $dec .= chr($c);
	        } else if ($c > 0x07FF) {
	            $dec .= chr(0xE0 | (($c >> 12) & 0x0F));
	            $dec .= chr(0x80 | (($c >>  6) & 0x3F));
	            $dec .= chr(0x80 | (($c >>  0) & 0x3F));
	        } else {
	            $dec .= chr(0xC0 | (($c >>  6) & 0x1F));
	            $dec .= chr(0x80 | (($c >>  0) & 0x3F));
	        }
	    }
	    return $dec;
	}	
}
 
?>

It’s ok for software to have a soul.

It’s ok for software to have a soul.
Or, more importantly, it’s ok to design and develop things that are injected with your beliefs, desires, and point of view.
We were in a team meeting today, discussing tuning some features for the next release build of rssTunes. Jack threw out something that I pretty vehemently disagreed with (specifically, having the software AutoTweet every individual track a user is listening to).  Immediately, without  doing A-B test, or supporting my case with an online survey, killed the idea dead in it’s track (pun).  If I had thought about it for a minute, I could have tried to justify it by explaining the app is growing out from a pretty niche community organically, and it’s probably not necessary at this stage for these circles. But in reality, I remembered seeing someone’s stream that listed every track they played as they played it- they did not stay in my friend list much longer. At any rate, as Creative Director of the project, it’s within my right to make decisions that affect the user experience.
Apparently, that right doesn’t exist everywhere. I just read this [http://www.lukew.com/ff/entry.asp?879] pretty interesting post about how things work in the design group at Facebook.  These bullets lept out at me:
- There is no creative director at Facebook, instead use a bottoms up process to get alignment. It’s critical for the team to share what they are doing.
Sharing is great. We love collaboration. We also like to throw titles aside and accept that good ideas come from anywhere, etc. But, taken to the extreme, like it apparently is with facebook, where A/B testing and office hours for guiding feedback- something else happens in the process. You end up with something utterly generic.
To be fair, it’s generic that works– you probably have different parameters when you have to service 250 mm users in however many countries. And let’s give it points for being infinitely more useable- and attractive than myspace- which is generic in a completely different way (instead of listening to users, they simply took every multimedia file rupert murdoch’s various media properties asked them to toss up on the page). But at the end of the day, no one is going to think Facebook looks especially awesome, or that it has a real point of view- other than being voted the least offensive option by the largest group of testers.
It doesn’t have to be this way. You don’t need to making skatedeck graphics or anime-influenced XBLA games to inject a healthy dose of personality.  http://slash7.com/  Amy Hoy, of Slash 7 has filled her time tracking application to the brim with personality- down the URL, http://letsfreckle.com/ LetsFreckle. Shaun Inman ’s stat tracking package, http://haveamint.com/ Mint, has a unique look and feel thats oozing style as well.  These are both serious web applications that anyone can pickup and use, but even a casual user can tell at a glance that SOMEONE made decisions.  a committe didn’t send Freckle off for A-B testing. Amy drew on her experience and insights, and put together her take on the future of time keeping.
There’s a point here, other than to make fun of Facebook. The rssTunes UI  is “neutral” by design at the moment. But now that we’ve got the backend wired up (for the most part) and begin to craft our next release, we’re not going to be A-B testing anything. We’re going to think hard about what’s worked and what doesn’t, and more importantly, what we like. As a silverlight app, we resolve to give it some personality, some soul. And we hope you like it.

Or, more importantly, it’s ok to design and develop things that are injected with your beliefs, desires, and point of view.

We were in a team meeting today, discussing tuning some features for the next release build of rssTunes. Jack threw out something that I pretty vehemently disagreed with (specifically, having the software AutoTweet every individual track a user is listening to).  Immediately, without  doing A-B tests, or supporting my case with an online survey, killed the idea dead in it’s track (pun).  If I had thought about it for a minute, I could have tried to justify it by explaining the app is growing out from a pretty niche community organically, and it’s probably not necessary at this stage for these circles. But in reality, I remembered seeing someone’s stream that listed every track they played as they played it- they did not stay in my friend list much longer. At any rate, as Creative Director of the project, it’s within my right to make decisions that affect the user experience.

Apparently, that right doesn’t exist everywhere. I just read this pretty interesting post about how things work in the design group at Facebook.  This bullet lept out at me:

There is no creative director at Facebook, instead use a bottoms up process to get alignment. It’s critical for the team to share what they are doing.

Sharing is great. We love collaboration. We also like to throw titles aside and accept that good ideas come from anywhere, etc. But, taken to the extreme, like it apparently is with facebook, where A/B testing and “office hours” guide design- something else happens in the process. You end up with something utterly generic.

To be fair, it’s generic that works– you probably have different parameters when you have to service 250 mm users in however many countries. And let’s give it points for being infinitely more useable- and attractive than myspace- which is generic in a completely different way (instead of listening to users, they simply took every multimedia file Rupert Murdoch’s various media properties asked them to toss up on the page).

But at the end of the day, no one is going to think Facebook looks especially awesome, or that it has a real point of view- other than being voted the least offensive option by the largest group of testers.

It doesn’t have to be this way.

You don’t need to be making skatedeck graphics or anime-influenced XBLA games to inject a healthy dose of personality.  Amy Hoy has filled her time tracking application to the brim with personality- down to the URL, LetsFreckle. Shaun Inman’s stat tracking package, Mint, has a unique (and functional) look and feel that’s oozing style as well.  These are both serious web applications that anyone can pickup and use, but even a casual user can tell at a glance that SOMEONE made decisions.

A committee didn’t send Freckle off for A-B testing. Amy drew on her experience and insights, and put together her take on the future of time keeping.

There’s a point here, other than to poke fun at Facebook. The rssTunes UI  is “neutral” by design at the moment. But now that we’ve got the backend wired up and working (for the most part) and begin to craft our next release, we’re not going to be A-B testing anything.

We’re going to think hard about what’s worked and what hasn’t.  And– more importantly– what we like. As a silverlight app, we resolve to give it some personality, some soul. We hope you like it- since we’re not going to be A-B testing it.

rssTunes beta launches!

It’s been a slow week on the blogging front, as the whole team has been hard at work finishing up the initial build of our newest toy, rssTunes.

rsstunesWhat is it?

It’s essentially an rss reader. Except instead of displaying text and images, it plays MP3s from music blogs around the web. We’ve picked a dozen or so of our favorite blogs to pull content from, and rssTunes supports individual user accounts, so you can go in and add/delete to your hears content. Want to find all metal blogs for the office, and something mellow for home? easy.  The coolest part is it fully supports Silverlight 3’s Out of browser feature, so you can pop it on your desktop and let it run in your dock.

Check it out (and help us kick the tires)!

It’s online right now at www.rsstunes.com, so please go give it a spin.

You know the drill with betas. We’re still tweaking a little bit on the backend, and we’re monkeying with ideas to spiffy up the front end. This is where you come in. Leave any feedback you have in the comments, or give us a shout on twitter @rsstunes.

They talk about this stuff better than I can.

I was trying to plan a second Sketchflow overview/tutorial, when I remembered Chris and Sara have a new book about this very topic due out in the near future. On their book website, they’ve uploaded a sample chapter with some source files you can download to play along at home.

protobookWhile I’ll still plan on sharing my experiences with Sketchflow, I’ll never trick anyone into thinking I’m capable of authoring an authoritative manual that breaks some fairly technical things down into easy to follow steps in natural language.  Chris and Sara do a fine job of that though, so if any of this Sketchflow chatter is even remotely intriguing, go download a demo of Blend with Sketchflow, and checkout the sample chapter of Chris and Sara’s book.

Disclosure: I’ve known Sara for years and think she’s pretty awesome.

Congrats to AKQA

AKQA’s new site for Beatles Rockband is the first Silverlight site to win an FWA award. You should go check it out here. I think a Beatles Rockband Happy Hour is in order soon!

Boxes and Arrows: Sketchflow Time

Over the years, I’ve worked with some fabulous IA and UX folks. An old friend of mine, Chinmoy, in particular would do elaborate large format renderings in fine ink– they were practically works of art. Jack has sworn by Fireworks for clickable pixel perfect demos in the past, and other use special single purpose software to make complex working demos of software concepts. Which is all awesome, but frequently overkill for planning a simple microsite or getting client approvals on a content plan.

I usually go super rough on a whiteboard, particularly in the first stages of figuring out a game plan. Then this will get translated into a flat, low-fi illustrator document and emailed over to a client for discussion. These crude, but effective enough documents resemble something like this:

nofiWhen we first heard about Silverlight 3, it was Sketchflow that got the oohs and ahhs around the office. If you haven’t seen it yet, Brad gives a pretty good overview of it at Brad’s Ramblings. It’s packed with features for developing fully animated prototypes, and should keep the UX community chattering awhile. Since it lives inside of Blend, all of the development power is there if you’re so inclined to get fancy, but I’m going to keep it fairly simple for now and simply try to replace my no-fi marker sketches with something slightly more polished.

The interface can be overwhelming, but it’s simple enough to create a custom workspace that only puts the tools you need onscreen.  Here’s the basic setup:

keyareasThe “boxes and arrows” live in the asset pile on the left. The Map at the bottom let’s you easily create new pages, and the main workspace lets you layout the page you’ve selected. (Tip: you can define the default SketchFlow page document size in the settings). You can safely ignore most of the rest of the app and use these core panels to get your simple IA projects underway.

A nice touch is the Sketchflow specific squiggly asset collection. While my chicken scratch is horrible (see figure a), spending too much time polishing results in infuriating conversations with all of the client stakeholders explaining that it’s not going to look like this, and no, the copy is not going to be in latin when the site goes live.

cropFrom this point on, it’s pretty intuitive to drag and drop your boxes around on the page. An advantage over Illustrator is that said boxes can have unique properties. If you grab the password box, it will give you a rectangular input box filled with obscured characters, the progress bar box lets you define the visible percentage (I assume if you are doing an animated walkthrough, it  can be fully functional, but again, not necessary for my semi-flat low fi wires). And of course, the button boxes predictably enough act as buttons- click on them to rename them, and right/ctrl click them to tell them which page in the SketchFlow map they should carry the user to on a click. Easy enough to figure out without having to resort to documentation!

Finally, when you’ve got your pages comped up, a menu command will package it up into the Sketchflow Player, a silverlight-based webpage that lets users click through a functional demo, as well as leave their comments and feedback.

sfplayerPretty cool. Again, there are resources out there for the hardcore nerds that want to build detailed UX prototypes. But it’s simple enough to pickup the basics that even casual IA dabblers should think about giving it a spin instead of spitting PDFs out of Illustrator.

.NET on the iPhone

The Mono project has just released an trial of MonoTouch, which lets us create .NET and C# applications for the iPhone. This makes me very happy in my special places. http://monotouch.net/

Fixing Fusion

If you’re running VMWare Fusion on Snow Leopard, you may have hit a few snags with MacFUSE and remote servers. Here’s how to fix it.