Media Center and Outlook

As I mentioned in my last post, I recently wrote an article for MSDN detailing an approach for synchronizing calendars in Outlook with custom event data sources.  I also mentioned that I included a few demo (but usable) CalendarProvider implementations for synchronizing with data sources like event logs, system restore points, RSS feeds, etc.

One CalendarProvider that I personally use, but that I didn't include with the article, allows me to view my Media Center recording schedule on an Outlook calendar.  This means I can see what and when my Media Center at home is planning to record (or what it has already recorded) and view that calendar side-by-side with my personal calendar!  If there's something I want to make sure I'm home for, I can just drag-and-drop it from the Media Center calendar onto my personal calendar.

Since I didn't include it with the article, I thought I'd walk through the plug-in here.  Note that it uses the recordings.xml file that Media Center uses to store your recordings information, and the schema for this file is undocumented and likely to change in future version… I'm purely walking you through my observations about the file format.

The Plug-In

The core of the plug-in itself is only a few lines.  I implemented a class that derives from my base CalendarProvider and that overrides the Configure and GetEvents methods.  The Configure method expects that the initialization data in the configuration file includes the location of the recordings.xml file, and simply stores the location to the _recordingsXmlPath private member.  The GetEvents method loads an array of all scheduled recordings, creates an Event object for each, and returns a collection of those events.  Very straightforward.

       public class MediaCenterCalendarProvider : CalendarProvider

       {

              private string _recordingsXmlPath;

              public override void Configure(string recordingsXmlPath)

{

                     _recordingsXmlPath = recordingsXmlPath;

              }

              public override EventCollection GetEvents()

              {

                     EventCollection events = new EventCollection();

                     if (_recordingsXmlPath != null)

                     {

                           RecordingScheduleItem [] items = RecordingsXmlReader.GetRecordings(_recordingsXmlPath);

                           foreach(RecordingScheduleItem item in items)

                           {

                                  Event e = new Event();

                                  e.Author = item.Channel;

                                  e.Location = item.Channel;

                                  e.Description = HttpUtility.HtmlDecode(item.Description);

                                  e.Title = item.Title;

                                  e.Start = item.Start;

                                  e.End = item.End;

                                  events.Add(e);

                           }

                     }

                     return events;

              }

       }

 

That’s it for the CalendarProvider itself; now all I need is the implementation of RecordingsXmlReader.GetRecordings.  This method starts by loading the recordings.xml file into an XmlDocument.  An ArrayList is also created to store all of the recordings found.

 

       public static RecordingScheduleItem [] GetRecordings(string recordingsXmlPath)

       {

              XmlDocument recordingsDoc = new XmlDocument();

              recordingsDoc.Load(recordingsXmlPath);

              ArrayList results = new ArrayList();

 

I then need to parse the document looking for scheduled recordings.  If you examine your recordings.xml file from MCE 2004, you’ll find a RecordingObjects node, under which lives a RecordingScheduleItems node, under which are a bunch of RecordingScheduleItem nodes.  Each of those RecordingScheduleItem nodes represents one recording (possibly future, possibly already recorded, possibly deleted, etc).  So I need to loop through all of those nodes.

 

            foreach(XmlNode scheduleItem in recordingsDoc.SelectNodes(

           @"RecordingObjects/RecordingScheduleItems/RecordingScheduleItem"))

       {

              XmlAttribute stateAttr = scheduleItem.Attributes["State"];

              if (stateAttr == null) continue;

              int state = int.Parse(stateAttr.Value);

              if (state == 128 || state == 1024 || state == 2048 || state == 4096) continue;

 

Note, though, that for my implementation, I only want to track certain kinds of the recordings, so I skip over those that have been deleted or canceled (0x80 and 0x400, respectively) and those that are repeats or redundant (0x800 and 0x1000, respectively), as specified by the State attribute on the RecordingScheduleItem.

 

I then grab lots of fun data from the node:

 

       XmlAttribute titleAttr = scheduleItem.Attributes["Title"];

       XmlAttribute startTimeAttr = scheduleItem.Attributes["StartTime"];

       XmlAttribute endTimeAttr = scheduleItem.Attributes["EndTime"];

XmlAttribute descriptionAttr = scheduleItem.Attributes["Description"];

       XmlAttribute episodeTitleAttr = scheduleItem.Attributes["EpisodeTitle"];

       XmlAttribute genreAttr = scheduleItem.Attributes["Genre"];

       XmlAttribute creditsAttr = scheduleItem.Attributes["Credits"];

       XmlAttribute ratingAttr = scheduleItem.Attributes["Rating"];

       XmlAttribute ratingReasonAttr = scheduleItem.Attributes["RatingReason"];

       XmlAttribute stationNameAttr = scheduleItem.Attributes["StationName"];

       XmlAttribute originalAirAttr = scheduleItem.Attributes["OriginalAir"];

       XmlAttribute prepaddingAttr = scheduleItem.Attributes["Prepadding"];

       XmlAttribute postpaddingAttr = scheduleItem.Attributes["PostPadding"];

       XmlAttribute callsignAttr = scheduleItem.Attributes["CallSign"];

       XmlAttribute tuningNumberAttr = scheduleItem.Attributes["TuningNumber"];

 

With all of this data, I can then massage it into the format that I need for an Event:

 

       // Get the title

       string title = (titleAttr != null) ? titleAttr.Value : string.Empty;

       string episodeTitle = (episodeTitleAttr != null) ? episodeTitleAttr.Value : string.Empty;

       if (episodeTitle.Length > 0) title += " (" + episodeTitle + ")";

       // Get the channel

       string channel = (stationNameAttr != null) ? stationNameAttr.Value : string.Empty;

       string channelNum = (tuningNumberAttr != null) ? tuningNumberAttr.Value : string.Empty;

       channel = (channel.Length > 0) ? channel + " [" + channelNum + "] " : channelNum;

       // Get the starting time

       DateTime startTime = DateTime.Parse(startTimeAttr.Value).ToUniversalTime();

       int prepadding = int.Parse(prepaddingAttr.Value);

       startTime = RoundTime(startTime.AddMinutes(prepadding / -60.0));

       // Get the ending time

       DateTime endTime = DateTime.Parse(endTimeAttr.Value).ToUniversalTime();

       int postpadding = int.Parse(postpaddingAttr.Value);

       endTime = RoundTime(endTime.AddMinutes(postpadding / -60.0));

       // Split the credits into various categories

       string actors = null, directors = null, otherCredits = null;

       if (creditsAttr != null)

       {

              string [] credits = creditsAttr.Value.Split(';');

              if (credits.Length > 0) actors = credits[0].Replace("/", ", ");

              if (credits.Length > 1) directors = credits[1].Replace("/", ", ");

              if (credits.Length > 2) otherCredits = string.Join("; ", credits, 2, credits.Length - 2);

       }

       // Get the description of the event

       StringBuilder description = new StringBuilder(512);

       description.Append("Title: " + title + Environment.NewLine);

       description.Append("Channel: " + channel + Environment.NewLine);

       description.Append("Date: " + startTime + " to " + endTime + Environment.NewLine);

       description.Append("Description: " + (descriptionAttr != null ? descriptionAttr.Value : string.Empty) + Environment.NewLine);

       if (actors != null && actors.Length > 0) description.Append("Actors: " + actors + Environment.NewLine);

       if (directors != null && directors.Length > 0) description.Append("Directors: " + directors + Environment.NewLine);

       if (otherCredits != null && directors.Length > 0) description.Append("Other Credits: " + otherCredits + Environment.NewLine);

       description.Append("Genre: " + (genreAttr != null ? genreAttr.Value : string.Empty) + Environment.NewLine);

       description.Append("Rating: " + (ratingAttr != null ? ratingAttr.Value : string.Empty) + Environment.NewLine);

       description.Append("Rating Reason: " + (ratingReasonAttr != null ? ratingReasonAttr.Value : string.Empty) + Environment.NewLine);

       description.Append("Original Air: " + (originalAirAttr != null ? originalAirAttr.Value : string.Empty) + Environment.NewLine);

 

and store it into one of my RecordingScheduleItem classes (simply a container for the data) which I then add to my results ArrayList.

 

       RecordingScheduleItem item = new RecordingScheduleItem();

       item.Title = title;

       item.Description = description.ToString();

       item.Channel = channel;

       item.Start = startTime;

       item.End = endTime;

       results.Add(item);

 

When I’m done, I simply return the list as an array back to the GetEvents method:

 

return (RecordingScheduleItem[])results.ToArray(typeof(RecordingScheduleItem));

 

I can then compile this, register the class in the web.config for the Web service (which runs on the MCE machine), and voila,  I can synchronize Outlook with my Media Center recording schedule! :)