Music Library Synchronization, Sonos Tips

I love Windows Home Server.   I’ve been using Windows Home Server for years, and just purchased a Windows Home Server 2011(WHS) box from Newegg (great deal on a HP Proliant micro server). 

image

Many have asked me why I like WHS so much – it’s NAS, it’s a media server, it’s backup.   It’s a step up from a simple NAS device (although, admittedly, not as plug and play), offers more flexibility and is more cost effective than a Drobo.  A small backup agent can take snapshots of your PC, typically on a daily basis, so they can be restored to a given point in time.  I keep snapshots of my initial installation, for example.  Restoring to those backups is a simple process. 

I’m also a big fan of Sonos, a whole-home music solution that works amazingly well.  What Sonos has done exceedingly well is blend quality hardware, quality software, and reasonable (but not cheap) price points.  I have an extensive music collection, and I point Sonos to a share on my WHS box to index and stream music. 

However, I consider my laptop my “database of record” for my music.  It’s where I download stuff, and I take it with me because I’m often on the road.  The problem I run into is keeping my WHS library in sync with my laptop.   In my case, I want to mirror my library on the WHS exactly as it is on my local collection – and because I’m often reorganizing my collection, adding tags, etc., I need a simple way to do this. 

Enter Robocopy.   Robocopy (Robust file copy) is now built into Windows, and it’s a simple command line tool with a number of options to make this a snap.   For example, if I want to mirror a folder on my laptop with my WHS, this command will do it:

c:\>robocopy "D:\Music" "\\BEAST\Sonos\music" /mir /r:10 /MT:8

D:\Music is my local folder, my server is \\Beast.  The /mir command is for mirror – it’s the same as using /purge and /e:  /purge is to delete files at the target folder that no longer exist in the source, and /e is to copy all subdirectories, including empty ones.   The /r:10 will tell it to retry up to 10 times, in case of some network glitch, and the /MT:8 will have Robocopy use 8 threads to speed things along.   (If you’re familiar with Robocopy, I don’t recommend using /z (restartable) mode as it adds overhead, not needed given the size of files we’re dealing with.)

Now, what if you don’t keep all your music local, and just want to copy it over?   You don’t want to use /mir since it will remove files you otherwise want to keep!   The rest of the command will work fine, but if you move/rename files locally that were previously copied, you’ll have to remember to do that manually on the server.  Once Robocopy does its thing, you’ll get a nice summary:

            Total Copied Skipped Mismatch FAILED Extras
Dirs : 1384 29 1355 0 0 0
Files : 15078 381 14697 0 0 0
Bytes : 117.188 g 3.212 g 113.975 g 0 0 0
Times : 0:16:40 0:02:52 0:00:00 0:00:48

Here, it copied about 30 new folders.  It took about 16 minutes to run, but that’s largely due to new content, having copied some 3.2gb of new files.   Assuming minor changes only, the process typically runs in about 30 seconds.

If you want to get fancy, you could even have Robocopy monitor your folders for changes. 

The next challenge is to have Sonos update its music index once new files are copied over.   Sonos can update its index on a daily basis (or manually via the control software), but I want it done automatically after new files are copied over.  This one is a bit trickier, but thanks to some gurus in the Sonos forums, it’s not impossible.   I’m including the .exe file here for you to use.  Obviously, trusting an exe from someone on the web is not something I’d do, but it’s a .NET assembly which means you can use a tool like JustDecompile to crack it open and look at the source yourself.   Having said that, I’m not responsible if this code causes your computer to blow up, your music collection to vanish, or kills any puppies.

The source code looks like so, and it sends an SOAP packet to a specified Sonos unit to trigger an index rebuild:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net;

namespace SonosIndexUpdater
{
    class Program
    {
       
        static void Main(string[] args)
        {
            string ip;

            if (args != null && args.Length > 0)
            {
                ip = args[0].Trim();
            }
            else
            {
                Console.WriteLine("Missing IP Address. Please add IP address for any Sonos unit.");
                return;
            }

            string header1 = @"SOAPACTION: ""urn:schemas-upnp-org:service
                :ContentDirectory:1#RefreshShareIndex""";
            string postData = @"<s:Envelope xmlns:s=""https://schemas.xmlsoap.org/soap/envelope/"" 
                s:encodingStyle=""https://schemas.xmlsoap.org/soap/encoding/"">
                <s:Body>
                <u:RefreshShareIndex xmlns:u=""urn:schemas-upnp-org:service:ContentDirectory:1"">
                <AlbumArtistDisplayOption></AlbumArtistDisplayOption></u:RefreshShareIndex>
                </s:Body>
                </s:Envelope>";
            string url = string.Format("https://{0}:1400/MediaServer/ContentDirectory/Control", ip);

            byte[] byteArray = Encoding.UTF8.GetBytes(postData);

            try
            {
                System.Net.WebRequest req = System.Net.WebRequest.Create(url);
                req.Headers.Add(header1);
                req.ContentType = "text/xml";
                req.Method = "POST";
                req.ContentType = "application/x-www-form-urlencoded";
                req.ContentLength = byteArray.Length;
                req.Timeout = 5000;

                Stream dataStream = req.GetRequestStream();

                dataStream.Write(byteArray, 0, byteArray.Length);
                dataStream.Close();

                using (WebResponse response = req.GetResponse())
                {
                    Console.WriteLine("Response from Sonos: {0}", 
                        ((HttpWebResponse)response).StatusDescription);

                    using (dataStream = response.GetResponseStream())
                    {
                        using (StreamReader reader = new StreamReader(dataStream))
                        {
                            string responseFromServer = reader.ReadToEnd();
                            Console.WriteLine("Data: {0}", responseFromServer);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception occured: {0}", ex.Message);
            }

        }
    }
}

 

To use it, you’d just pass in the IP address of any Sonos unit:

 

c:\>SonosIndexUpdater 192.168.1.100

If you stumbled on this and aren’t a developer but want to try it out, you can build this for free using Visual Studio Express.    Here are some files:

 

EXE file only: Download
VS2010 Solution: Download