[banshee/podcast-ng] Initial podcast-ng commit.



commit 2865865346ddc555393d1ad9c63dd323ae0b9279
Author: Mike Urbanski <michael c urbanski gmail com>
Date:   Mon Mar 29 18:29:05 2010 -0500

    Initial podcast-ng commit.

 src/Extensions/Banshee.Paas/Banshee.Paas.addin.xml |   26 +
 src/Extensions/Banshee.Paas/Banshee.Paas.csproj    |  205 +++++
 .../Banshee.Paas.Aether/AetherClient.cs            |   73 ++
 .../Banshee.Paas.Aether/AetherClientID.cs          |   36 +
 .../Banshee.Paas.Aether/AetherClientState.cs       |   36 +
 .../AetherClientStateChangedEventArgs.cs           |   45 ++
 .../Banshee.Paas.Aether/AetherRequest.cs           |  310 ++++++++
 .../AetherRequestCompletedEventArgs.cs             |   55 ++
 .../Banshee.Paas.Aether/ChannelEventArgs.cs        |   57 ++
 .../Banshee.Paas.Aether/ChannelUpdateStatus.cs     |   37 +
 .../Banshee.Paas.Aether/ItemEventArgs.cs           |   57 ++
 .../GetCategoriesCompletedEventArgs.cs             |   47 ++
 .../MiroGuideClient/GetChannelsEventArgs.cs        |   54 ++
 .../MiroGuideClient/MiroGuideAccountInfo.cs        |  107 +++
 .../MiroGuideClient/MiroGuideCategoryInfo.cs       |   51 ++
 .../MiroGuideClient/MiroGuideChannelInfo.cs        |  111 +++
 .../MiroGuideClient/MiroGuideClient.cs             |  750 ++++++++++++++++++
 .../MiroGuideClient/MiroGuideClientError.cs        |   36 +
 .../MiroGuideClient/MiroGuideClientMethod.cs       |   37 +
 .../MiroGuideClient/MiroGuideFilterType.cs         |   44 +
 .../MiroGuideMethodCompletedEventArgs.cs           |   46 ++
 .../MiroGuideClient/MiroGuideRequestState.cs       |  132 +++
 .../MiroGuideClient/MiroGuideSortType.cs           |   15 +
 .../MiroGuideClient/RequestCompletedEventArgs.cs   |   55 ++
 .../MiroGuideClient/SearchContext.cs               |  126 +++
 .../MiroGuideClient/ServiceMethodFlags.cs          |   37 +
 .../SubscriptionRequestedEventArgs.cs              |   55 ++
 .../Banshee.Paas.Aether/RequestState.cs            |  141 ++++
 .../ChannelUpdateCompletedEventArgs.cs             |   53 ++
 .../SyndicationClient/ChannelUpdateManager.cs      |   50 ++
 .../SyndicationClient/ChannelUpdateTask.cs         |  123 +++
 .../SyndicationClient/ItmsPodcast.cs               |  100 +++
 .../SyndicationClient/RssParser.cs                 |  286 +++++++
 .../SyndicationClient/SyndicationClient.cs         |  532 +++++++++++++
 .../Banshee.Paas.Data/CacheModelProvider.cs        |  142 ++++
 .../Banshee.Paas.Data/CacheableItem.cs             |   54 ++
 .../Banshee.Paas.Data/DownloadPreference.cs        |   35 +
 .../Banshee.Paas.Data/DownloadStatusFilterModel.cs |   86 ++
 .../Banshee.Paas/Banshee.Paas.Data/ListModel.cs    |  201 +++++
 .../Banshee.Paas/Banshee.Paas.Data/PaasChannel.cs  |  223 ++++++
 .../Banshee.Paas.Data/PaasChannelModel.cs          |   62 ++
 .../Banshee.Paas/Banshee.Paas.Data/PaasItem.cs     |  323 ++++++++
 .../Banshee.Paas.Data/PaasTrackInfo.cs             |  189 +++++
 .../Banshee.Paas.Data/PaasTrackListModel.cs        |  100 +++
 .../Banshee.Paas.Data/PaasUnheardFilterModel.cs    |   85 ++
 .../Banshee.Paas.Data/SingletonSelection.cs        |   47 ++
 .../QueuedDownloadTask.cs                          |   86 ++
 .../DownloadListView.cs                            |  125 +++
 .../DownloadManagerInterface.cs                    |  191 +++++
 .../DownloadSource.cs                              |   92 +++
 .../DownloadSourceContents.cs                      |   81 ++
 .../DownloadUserJob.cs                             |  147 ++++
 .../DownloadListModel.cs                           |   74 ++
 .../PaasDownloadManager.cs                         |  365 +++++++++
 .../Banshee.Paas.Gui/ColumnCellChannel.cs          |  197 +++++
 .../Banshee.Paas.Gui/ColumnCellDownloadStatus.cs   |   56 ++
 .../ColumnCellPaasStatusIndicator.cs               |  123 +++
 .../Banshee.Paas.Gui/ColumnCellPublished.cs        |   41 +
 .../Banshee.Paas.Gui/ColumnCellUnheard.cs          |   57 ++
 .../Dialogs/ChannelPropertiesDialog.cs             |  267 +++++++
 .../Banshee.Paas.Gui/Dialogs/SubscribeDialog.cs    |  186 +++++
 .../Banshee.Paas.Gui/DownloadPreferenceComboBox.cs |   62 ++
 .../Banshee.Paas.Gui/DownloadStatusFilterView.cs   |   49 ++
 .../Banshee.Paas.Gui/IColumnCellDataHelper.cs      |   39 +
 .../Banshee.Paas/Banshee.Paas.Gui/PaasActions.cs   |  793 +++++++++++++++++++
 .../Banshee.Paas.Gui/PaasChannelView.cs            |   78 ++
 .../Banshee.Paas.Gui/PaasColumnController.cs       |  100 +++
 .../Banshee.Paas/Banshee.Paas.Gui/PaasItemPage.cs  |  127 +++
 .../Banshee.Paas/Banshee.Paas.Gui/PaasItemView.cs  |   93 +++
 .../Banshee.Paas.Gui/PaasSourceContents.cs         |  153 ++++
 .../Banshee.Paas.Gui/PaasUnheardFilterView.cs      |   49 ++
 .../ChannelInfoPreview.cs                          |   88 ++
 .../ColumnCellChannel.cs                           |  166 ++++
 .../MiroGuideAccountDialog.cs                      |  153 ++++
 .../Banshee.Paas.MiroGuide.Gui/MiroGuideActions.cs |  197 +++++
 .../MiroGuideCategoryListView.cs                   |   51 ++
 .../MiroGuideChannelListView.cs                    |   63 ++
 .../MiroGuideLoginForm.cs                          |  143 ++++
 .../MiroGuideSearchEntry.cs                        |   55 ++
 .../ReflectionInfoWidget.cs                        |  164 ++++
 .../SortPreferenceActionButton.cs                  |   79 ++
 .../SortPreferenceChangedEventArgs.cs              |   51 ++
 .../SourceContents/BrowserSourceContents.cs        |   98 +++
 .../SourceContents/ChannelSourceContents.cs        |  183 +++++
 .../SourceContents/MiroGuideSourceContents.cs      |   71 ++
 .../MiroGuideCategoryListModel.cs                  |   41 +
 .../MiroGuideChannelListModel.cs                   |   46 ++
 .../MiroGuideImageFetchJob.cs                      |   86 ++
 .../MiroGuideInterfaceManager.cs                   |   74 ++
 .../MiroGuideSearchFilter.cs                       |   13 +
 .../Sources/BrowseChannelsSource.cs                |  135 ++++
 .../Sources/ChannelSource.cs                       |  419 ++++++++++
 .../Sources/FeaturedChannelsSource.cs              |   61 ++
 .../Sources/HDChannelsSource.cs                    |   62 ++
 .../Sources/MiroGuideSource.cs                     |   50 ++
 .../Sources/MiroGuideSourcePosition.cs             |   41 +
 .../Sources/PopularChannelsSource.cs               |   61 ++
 .../Sources/RecommendedChannelsSource.cs           |   46 ++
 .../Banshee.Paas.MiroGuide/Sources/SearchSource.cs |  156 ++++
 .../Sources/TopRatedChannelsSource.cs              |   61 ++
 .../Banshee.Paas/Banshee.Paas.Utils/OpmlParser.cs  |   93 +++
 .../Banshee.Paas/Banshee.Paas.Utils/StringUtils.cs |   54 ++
 .../Banshee.Paas/Banshee.Paas/PaasImageFetchJob.cs |   83 ++
 .../Banshee.Paas/Banshee.Paas/PaasService.cs       |  833 +++++++++++++++++++
 .../Banshee.Paas/Banshee.Paas/PaasSource.cs        |  277 +++++++
 src/Extensions/Banshee.Paas/Makefile.am            |  123 +++
 .../Banshee.Paas/Resources/ActiveSourceUI.xml      |   39 +
 src/Extensions/Banshee.Paas/Resources/GlobalUI.xml |   43 +
 .../Resources/MiroGuideActiveSourceUI.xml          |    7 +
 .../Banshee.Paas/Resources/MiroGuideUI.xml         |   23 +
 .../ThemeIcons/16x16/categories/podcast.png        |  Bin 0 -> 949 bytes
 .../ThemeIcons/16x16/status/podcast-new.png        |  Bin 0 -> 533 bytes
 .../ThemeIcons/22x22/categories/podcast.png        |  Bin 0 -> 1564 bytes
 .../ThemeIcons/48x48/categories/podcast.png        |  Bin 0 -> 4779 bytes
 .../ThemeIcons/scalable/categories/miro-browse.svg |  232 ++++++
 .../ThemeIcons/scalable/categories/miro.svg        |  188 +++++
 .../categories/miroguide-default-channel.svg       |  318 ++++++++
 src/Libraries/Migo2/Makefile.am                    |   55 ++
 .../Migo2/Migo2.Async/AsyncStateManager.cs         |  127 +++
 .../Migo2.Async/CommandQueue/CommandDelegate.cs    |   32 +
 .../Migo2/Migo2.Async/CommandQueue/CommandQueue.cs |  268 +++++++
 .../Migo2.Async/CommandQueue/CommandWrapper.cs     |   50 ++
 .../Migo2/Migo2.Async/CommandQueue/EventWrapper.cs |   56 ++
 .../Migo2/Migo2.Async/CommandQueue/ICommand.cs     |   33 +
 .../Migo2/Migo2.Async/Task/CancellationType.cs     |   38 +
 .../Migo2/Migo2.Async/Task/IWaitableTask.cs        |   36 +
 src/Libraries/Migo2/Migo2.Async/Task/Task.cs       |  393 +++++++++
 .../Migo2.Async/Task/TaskCompletedEventArgs.cs     |   90 +++
 .../Migo2/Migo2.Async/Task/TaskEventArgs.cs        |   56 ++
 src/Libraries/Migo2/Migo2.Async/Task/TaskState.cs  |   46 ++
 .../Migo2.Async/Task/TaskStateChangedEventArgs.cs  |   73 ++
 .../EventArgs/GroupStatusChangedEventArgs.cs       |   58 ++
 .../TaskGroup/EventArgs/ManipulatedEventArgs.cs    |   78 ++
 .../TaskGroup/EventArgs/ReorderedEventArgs.cs      |   46 ++
 .../TaskGroup/EventArgs/TaskAddedEventArgs.cs      |   81 ++
 .../EventArgs/TaskProgressChangedEventArgs.cs      |   54 ++
 .../TaskGroup/EventArgs/TaskRemovedEventArgs.cs    |   79 ++
 .../Migo2.Async/TaskGroup/GroupProgressManager.cs  |  176 ++++
 .../Migo2.Async/TaskGroup/GroupStatusManager.cs    |  349 ++++++++
 .../Migo2/Migo2.Async/TaskGroup/TaskGroup.cs       |  690 ++++++++++++++++
 .../Migo2.Async/TaskGroup/TaskGroup_Collection.cs  |  536 +++++++++++++
 .../Migo2/Migo2.Collections/OrderComparer.cs       |   53 ++
 src/Libraries/Migo2/Migo2.Collections/Pair.cs      |   43 +
 .../Migo2.DownloadService/DownloadStatusManager.cs |   72 ++
 .../DownloadTaskStatusUpdatedEventArgs.cs          |   53 ++
 .../Migo2.DownloadService/HttpDownloadGroup.cs     |  233 ++++++
 .../HttpDownloadGroupStatusChangedEventArgs.cs     |   49 ++
 .../Migo2.DownloadService/HttpDownloadManager.cs   |  164 ++++
 .../HttpFileDownloadErrors.cs                      |   38 +
 .../Migo2.DownloadService/HttpFileDownloadTask.cs  |  436 ++++++++++
 .../Migo2.Net/AsyncWebClient/AsyncWebClient.cs     |  834 ++++++++++++++++++++
 .../DownloadDataCompletedEventArgs.cs              |   49 ++
 .../DownloadProgressChangedEventArgs.cs            |   61 ++
 .../Migo2.Net/AsyncWebClient/DownloadStatus.cs     |   75 ++
 .../DownloadStatusUpdatedEventArgs.cs              |   51 ++
 .../DownloadStringCompletedEventArgs.cs            |   53 ++
 .../AsyncWebClient/RemoteFileModifiedException.cs  |   59 ++
 .../AsyncWebClient/TransferStatusManager.cs        |  213 +++++
 .../AsyncWebClient/TransferStatusManager_Rate.cs   |  161 ++++
 src/Libraries/Migo2/Migo2.Utils/Rfc822DateTime.cs  |  199 +++++
 src/Libraries/Migo2/Migo2.Utils/UnitUtils.cs       |   70 ++
 src/Libraries/Migo2/Migo2.Utils/XmlUtils.cs        |  102 +++
 src/Libraries/Migo2/Migo2.csproj                   |   98 +++
 src/Libraries/Migo2/README.png                     |  Bin 0 -> 5864 bytes
 164 files changed, 20996 insertions(+), 0 deletions(-)
---
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas.addin.xml b/src/Extensions/Banshee.Paas/Banshee.Paas.addin.xml
new file mode 100644
index 0000000..2e089c8
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas.addin.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Addin 
+    id="Banshee.Paas"
+    version="1.0"
+    compatVersion="1.0"
+    copyright="All copyrights held by respective authors / owners.  Licensed under the MIT X11 license."
+    name="Podcasting"
+    category="User Interface"
+    description="Subscribe to and manage your podcasts!"
+    author="Aaron Bockover, Gabriel Burt, Brandan Lloyd, Bertrand Lorentz, Brian Lucas, John Millikin, Mike Urbanski"
+    url="http://banshee-project.org/";
+    defaultEnabled="true">
+
+  <Dependencies>
+    <Addin id="Banshee.Services" version="1.0"/>
+    <Addin id="Banshee.ThickClient" version="1.0"/>
+  </Dependencies>
+
+  <Extension path="/Banshee/ServiceManager/Service">
+    <Service class="Banshee.Paas.PaasService"/>
+  </Extension>
+
+  <Extension path="/Banshee/Gui/TrackEditor/NotebookPage">
+    <TrackEditorPage class="Banshee.Paas.Gui.PaasItemPage"/>
+  </Extension>
+</Addin>
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas.csproj b/src/Extensions/Banshee.Paas/Banshee.Paas.csproj
new file mode 100644
index 0000000..5c9a253
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas.csproj
@@ -0,0 +1,205 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"; ToolsVersion="3.5">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>9.0.21022</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{EEA0F24F-3180-4C63-993C-84179711C7EF}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AssemblyName>Banshee.Paas</AssemblyName>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>none</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="gdk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
+    <Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
+    <Reference Include="Mono.Cairo" />
+    <Reference Include="pango-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
+    <Reference Include="System.Xml.Linq">
+      <RequiredTargetFramework>3.5</RequiredTargetFramework>
+    </Reference>
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ProjectExtensions>
+    <MonoDevelop>
+      <Properties>
+        <MonoDevelop.Autotools.MakefileInfo IntegrationEnabled="true" RelativeMakefileName="Makefile.am" IsAutotoolsProject="true" RelativeConfigureInPath="../../..">
+          <BuildFilesVar Sync="true" Name="SOURCES" />
+          <DeployFilesVar />
+          <ResourcesVar Name="RESOURCES" />
+          <OthersVar />
+          <GacRefVar />
+          <AsmRefVar />
+          <ProjectRefVar />
+          <MessageRegex Name="Vala" />
+        </MonoDevelop.Autotools.MakefileInfo>
+      </Properties>
+    </MonoDevelop>
+  </ProjectExtensions>
+  <ItemGroup>
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\AetherClient.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\AetherClientID.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\AetherClientState.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\AetherClientStateChangedEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideClient.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\ServiceMethodFlags.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\SyndicationClient\ChannelUpdateManager.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\SyndicationClient\ChannelUpdateTask.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\SyndicationClient\RssParser.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\SyndicationClient\SyndicationClient.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\PaasChannel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\PaasChannelModel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\PaasItem.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\PaasTrackInfo.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\PaasTrackListModel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\Dialogs\SubscribeDialog.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\ColumnCellChannel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\ColumnCellPaasStatusIndicator.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\ColumnCellPublished.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\PaasActions.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\PaasChannelView.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\PaasColumnController.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\PaasItemView.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\PaasSourceContents.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Utils\StringUtils.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\SyndicationClient\ChannelUpdateCompletedEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\PaasItemPage.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\Dialogs\ChannelPropertiesDialog.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\DownloadPreference.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.DownloadManager\PaasDownloadManager.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.DownloadManager.Gui\DownloadListView.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.DownloadManager.Gui\DownloadManagerInterface.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.DownloadManager.Gui\DownloadSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.DownloadManager.Gui\DownloadSourceContents.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.DownloadManager.Gui\DownloadUserJob.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\IColumnCellDataHelper.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\PaasUnheardFilterModel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\PaasUnheardFilterView.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\ColumnCellUnheard.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\ColumnCellDownloadStatus.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\DownloadStatusFilterModel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\DownloadStatusFilterView.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas\PaasImageFetchJob.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas\PaasService.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas\PaasSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\SyndicationClient\ItmsPodcast.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.DownloadManager\DownloadListModel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.DownloadManager.Data\QueuedDownloadTask.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\AetherRequest.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\AetherRequestCompletedEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\RequestState.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideRequestState.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideClientMethod.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\ChannelEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideClientError.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideAccountInfo.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\ItemEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\ChannelUpdateStatus.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Gui\DownloadPreferenceComboBox.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\ListModel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideChannelInfo.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Utils\OpmlParser.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\ColumnCellChannel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\MiroGuideAccountDialog.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\MiroGuideActions.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\MiroGuideChannelListView.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\MiroGuideLoginForm.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\MiroGuideSearchEntry.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideFilterType.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideSortType.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\GetChannelsEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideMethodCompletedEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\RequestCompletedEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\SubscriptionRequestedEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\MiroGuideImageFetchJob.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\MiroGuideChannelListModel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\SearchContext.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\MiroGuideInterfaceManager.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\SearchSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\ChannelSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\HDChannelsSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\FeaturedChannelsSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\MiroGuideSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\PopularChannelsSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\TopRatedChannelsSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\SourceContents\ChannelSourceContents.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\SourceContents\MiroGuideSourceContents.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\MiroGuideSourcePosition.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\BrowseChannelsSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\RecommendedChannelsSource.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\SortPreferenceActionButton.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\SortPreferenceChangedEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\MiroGuideCategoryListView.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\MiroGuideCategoryInfo.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Aether\MiroGuideClient\GetCategoriesCompletedEventArgs.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\MiroGuideCategoryListModel.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\SourceContents\BrowserSourceContents.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide\MiroGuideSearchFilter.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\SingletonSelection.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\CacheModelProvider.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.Data\CacheableItem.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\ReflectionInfoWidget.cs" />
+    <Compile Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\ChannelInfoPreview.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\Core\Banshee.Core\Banshee.Core.csproj">
+      <Project>{2ADB831A-A050-47D0-B6B9-9C19D60233BB}</Project>
+      <Name>Banshee.Core</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Core\Banshee.Services\Banshee.Services.csproj">
+      <Project>{B28354F0-BA87-44E8-989F-B864A3C7C09F}</Project>
+      <Name>Banshee.Services</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Libraries\Hyena.Gui\Hyena.Gui.csproj">
+      <Project>{C856EFD8-E812-4E61-8B76-E3583D94C233}</Project>
+      <Name>Hyena.Gui</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Libraries\Migo2\Migo2.csproj">
+      <Project>{FC311410-8638-4A66-A8A5-1E900CDC6C7B}</Project>
+      <Name>Migo2</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Core\Banshee.ThickClient\Banshee.ThickClient.csproj">
+      <Project>{AC839523-7BDF-4AB6-8115-E17921B96EC6}</Project>
+      <Name>Banshee.ThickClient</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Libraries\Hyena\Hyena.csproj">
+      <Project>{95374549-9553-4C1E-9D89-667755F90E12}</Project>
+      <Name>Hyena</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="Resources\ActiveSourceUI.xml" />
+    <None Include="Resources\GlobalUI.xml" />
+    <None Include="Banshee.Paas.addin.xml" />
+    <None Include="Resources\MiroGuideUI.xml" />
+    <None Include="Resources\MiroGuideActiveSourceUI.xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Folder Include="Banshee.Paas\Banshee.Paas.DownloadManager\" />
+    <Folder Include="Banshee.Paas\Banshee.Paas\" />
+    <Folder Include="Banshee.Paas\Banshee.Paas.DownloadManager.Data\" />
+    <Folder Include="Banshee.Paas\Banshee.Paas.MiroGuide\" />
+    <Folder Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\" />
+    <Folder Include="Banshee.Paas\Banshee.Paas.MiroGuide\Sources\" />
+    <Folder Include="Banshee.Paas\Banshee.Paas.MiroGuide.Gui\SourceContents\" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClient.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClient.cs
new file mode 100644
index 0000000..c19540d
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClient.cs
@@ -0,0 +1,73 @@
+//
+// AetherClient.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Migo2.Async;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Aether
+{
+    public abstract class AetherClient : IDisposable
+    {
+        private CommandQueue event_queue;
+        private readonly object sync = new object ();
+
+        protected object SyncRoot {
+            get { return sync; }
+        }
+
+        protected CommandQueue EventQueue {
+            get { return event_queue; }
+        }
+
+        public event EventHandler<AetherClientStateChangedEventArgs> StateChanged;
+
+        public AetherClient ()
+        {
+            event_queue = new CommandQueue ();
+        }
+
+        public virtual void Dispose ()
+        {
+            event_queue.Dispose ();
+            event_queue = null;
+        }
+
+        protected virtual void OnStateChanged (AetherClientState oldState, AetherClientState newState)
+        {
+            var handler = StateChanged;
+
+            if (handler != null) {
+                event_queue.Register (new EventWrapper<AetherClientStateChangedEventArgs> (
+                    handler, this, new AetherClientStateChangedEventArgs (oldState, newState)
+                ));
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClientID.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClientID.cs
new file mode 100644
index 0000000..28a0c4c
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClientID.cs
@@ -0,0 +1,36 @@
+//
+// AetherClientID.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.Aether
+{
+    public enum AetherClientID : int
+    {
+        Syndication = 1, // RSS, ATOM, etc.
+        MiroGuide   = 2
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClientState.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClientState.cs
new file mode 100644
index 0000000..b5aef03
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClientState.cs
@@ -0,0 +1,36 @@
+//
+// AetherClientState.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.Aether
+{
+    public enum AetherClientState
+    {
+        Busy,
+        Idle
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClientStateChangedEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClientStateChangedEventArgs.cs
new file mode 100644
index 0000000..81fdce7
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherClientStateChangedEventArgs.cs
@@ -0,0 +1,45 @@
+//
+// AetherClientStateChangedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.Aether
+{
+    public class AetherClientStateChangedEventArgs : EventArgs
+    {
+        private readonly AetherClientState new_state;
+        private readonly AetherClientState old_state;
+
+        public AetherClientState NewState { get { return new_state; } }
+        public AetherClientState OldState { get { return old_state; } }
+
+        public AetherClientStateChangedEventArgs (AetherClientState oldState, AetherClientState newState)
+        {
+            old_state = oldState;
+            new_state = newState;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherRequest.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherRequest.cs
new file mode 100644
index 0000000..976bd36
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherRequest.cs
@@ -0,0 +1,310 @@
+//
+// AetherRequest.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+
+using System.Collections;
+using System.Collections.Generic;
+
+using Hyena;
+using Migo2.Async;
+
+namespace Banshee.Paas.Aether
+{
+    public enum HttpMethod
+    {
+        GET  = 0,
+        POST = 1
+    }
+
+    public sealed class AetherRequest
+    {
+        private int timeout;
+        private AsyncStateManager asm;
+
+        private HttpWebRequest request;
+        private CookieContainer cookie_container;
+
+        public EventHandler<AetherRequestCompletedEventArgs> Completed;
+
+        public CookieContainer CookieContainer {
+            get { return cookie_container; }
+            set { cookie_container = value; }
+        }
+
+        public string ContentType { get; set; }
+        public ICredentials Credentials { get; set; }
+
+        public int Timeout {
+            get { return timeout; }
+            set {
+                if (value < 0) {
+                    throw new ArgumentOutOfRangeException ("Timeout", "Must be greater than 0.");
+                }
+
+                timeout = value;
+            }
+        }
+
+        public string UserAgent { get; set; }
+
+        public AetherRequest ()
+        {
+            asm = new AsyncStateManager ();
+        }
+
+        public void BeginGetRequest (Uri uri)
+        {
+            CryOutThroughTheAetherAsync (uri, HttpMethod.GET, null, null);
+        }
+
+        public void BeginGetRequest (Uri uri, object userState)
+        {
+            CryOutThroughTheAetherAsync (uri, HttpMethod.GET, null, userState);
+        }
+
+        public void BeginPostRequest (Uri uri, byte[] postData)
+        {
+            CryOutThroughTheAetherAsync (uri, HttpMethod.POST, postData, null);
+        }
+
+        public void BeginPostRequest (Uri uri, byte[] postData, object userState)
+        {
+            CryOutThroughTheAetherAsync (uri, HttpMethod.POST, postData, userState);
+        }
+
+        public void CancelAsync ()
+        {
+            if (asm.SetCancelled ()) {
+                Abort ();
+            }
+        }
+
+        private void Abort ()
+        {
+            HttpWebRequest req = request;
+
+            if (req != null) {
+                req.Abort ();
+            }
+        }
+
+        private void RequestCompleted (RequestState state)
+        {
+            try {
+                asm.SetCompleted ();
+                OnCompleted (CreateAetherArgs (state));
+            } finally {
+                request = null;
+                state.Dispose ();
+                asm.Reset ();
+            }
+        }
+
+        private void HandleException (RequestState state, Exception e)
+        {
+            state.Error = e;
+            RequestCompleted (state);
+        }
+
+        private void CryOutThroughTheAetherAsync (Uri uri, HttpMethod method, byte[] postData, object userState)
+        {
+            asm.SetBusy ();
+
+            RequestState state = new RequestState () {
+                UserState = new object[2] { null, userState }
+            };
+
+            try {
+                request = WebRequest.Create (uri) as HttpWebRequest;
+
+                if (cookie_container != null) {
+                    request.CookieContainer = cookie_container;
+                }
+
+                request.Timeout = Timeout;
+                request.UserAgent = UserAgent;
+                request.Credentials = Credentials;
+
+                request.AllowAutoRedirect = true;
+
+                state.Request = request;
+
+                switch (method) {
+                case HttpMethod.GET:
+                    request.Method = "GET";
+                    GetResponse (state);
+                    break;
+                case HttpMethod.POST:
+                    request.Method = "POST";
+                    request.ContentType = ContentType;
+                    SendRequest (postData, state);
+                    break;
+                }
+            } catch (Exception e) {
+                state.Error = e;
+                RequestCompleted (state);
+            }
+        }
+
+        private void SendRequest (byte[] data, RequestState state)
+        {
+            state.WriteBuffer = data;
+            state.AddTimeout (OnTimeout, timeout, true, state);
+            request.BeginGetRequestStream (FuckitIJustNeedAName, state);
+        }
+
+        private void GetResponse (RequestState state)
+        {
+            state.AddTimeout (OnTimeout, timeout, true, state);
+            request.BeginGetResponse (ThisIsCthulhuGoAheadCaller, state);
+        }
+
+        private void FuckitIJustNeedAName (IAsyncResult ar) {
+            RequestState state = ar.AsyncState as RequestState;
+
+            try {
+                state.RemoveTimeout (true);
+                state.RequestStream = state.Request.EndGetRequestStream (ar);
+
+                state.AddTimeout (OnTimeout, timeout, false, state);
+                state.RequestStream.BeginWrite (state.WriteBuffer, 0, state.WriteBuffer.Length, EndWrite, state);
+            } catch (Exception e) {
+                HandleException (state, e);
+            }
+        }
+
+        private void ThisIsCthulhuGoAheadCaller (IAsyncResult ar)
+        {
+            // Wow, ok, hi.  Uhh, first time caller, longtime dreamer of mad dreams.
+            RequestState state = ar.AsyncState as RequestState;
+
+            try {
+                state.RemoveTimeout (true);
+                state.Response = state.Request.EndGetResponse (ar) as HttpWebResponse;
+                state.ResponseStream = state.Response.GetResponseStream ();
+
+                state.AddTimeout (OnTimeout, timeout, false, state);
+                state.ResponseStream.BeginRead (state.ReadBuffer, 0, RequestState.BufferSize, EndRead, state);
+            } catch (Exception e) {
+                HandleException (state, e);
+            }
+        }
+
+        private void EndRead (IAsyncResult ar)
+        {
+            int nread = -1;
+            RequestState state = ar.AsyncState as RequestState;
+
+            try {
+                state.SetTimeoutHandle ();
+                nread = state.ResponseStream.EndRead (ar);
+
+                if (nread != 0) {
+                    state.ReadData.Write (state.ReadBuffer, 0, nread);
+                    state.ResponseStream.BeginRead (state.ReadBuffer, 0, RequestState.BufferSize, EndRead, state);
+                } else {
+                    ((object[])(state.UserState))[0] = state.ReadData.ToArray ();
+                    RequestCompleted (state);
+                }
+            } catch (Exception e) {
+                HandleException (state, e);
+            }
+        }
+
+        private void EndWrite (IAsyncResult ar)
+        {
+            RequestState state = ar.AsyncState as RequestState;
+
+            try {
+                state.RemoveTimeout (true);
+                state.RequestStream.EndWrite (ar);
+
+                if (state.RequestStream != null) {
+                    state.RequestStream.Close ();
+                    state.RequestStream = null;
+                }
+
+                GetResponse (state);
+            } catch (Exception e) {
+                HandleException (state, e);
+            }
+        }
+
+        private AetherRequestCompletedEventArgs CreateAetherArgs (RequestState state)
+        {
+            byte[] data = null;
+            Exception err = null;
+            bool timedout = false;
+            bool cancelled = false;
+
+            object[] userState = state.UserState as object[];
+
+            if (asm.Cancelled) {
+                cancelled = true;
+            } else if (asm.Timedout) {
+                timedout = true;
+            } else {
+                if (state.Error != null) {
+                    if (state.Error is WebException) {
+                        WebException e = state.Error as WebException;
+
+                        if (e.Status == WebExceptionStatus.Timeout) {
+                            timedout = true;
+                        }
+                    }
+                }
+
+                if (!timedout) {
+                    err  = state.Error;
+                    data = userState[0] as byte[];
+                }
+            }
+
+            return new AetherRequestCompletedEventArgs (data, err, cancelled, timedout, userState[1]);
+        }
+
+        private void OnTimeout (object userState, bool timedOut)
+        {
+            if (timedOut) {
+                if (asm.SetTimedout ()) {
+                    Abort ();
+                }
+            }
+        }
+
+        private void OnCompleted (AetherRequestCompletedEventArgs e) {
+            var handler = Completed;
+
+            if (handler != null) {
+                handler (this, e);
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherRequestCompletedEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherRequestCompletedEventArgs.cs
new file mode 100644
index 0000000..93fe226
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/AetherRequestCompletedEventArgs.cs
@@ -0,0 +1,55 @@
+//
+// AetherAsyncCompletedEventArgs.cs - Way too fucking long.
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.ComponentModel;
+
+namespace Banshee.Paas.Aether
+{
+    public class AetherRequestCompletedEventArgs : AsyncCompletedEventArgs
+    {
+        private readonly byte[] data;
+        private readonly bool timedout;
+
+        public byte[] Data {
+            get { return data; }
+        }
+
+        public bool Timedout {
+            get { return timedout; }
+        }
+
+        public AetherRequestCompletedEventArgs (byte[] data,
+                                                Exception err,
+                                                bool cancelled,
+                                                bool timedout,
+                                                object userState) : base (err, cancelled, userState)
+        {
+            this.data = data;
+            this.timedout = timedout;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/ChannelEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/ChannelEventArgs.cs
new file mode 100644
index 0000000..60a5d07
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/ChannelEventArgs.cs
@@ -0,0 +1,57 @@
+//
+// ChannelEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Aether
+{
+    public class ChannelEventArgs : EventArgs
+    {
+        private readonly PaasChannel channel;
+        private readonly IEnumerable<PaasChannel> channels;
+
+        public PaasChannel Channel {
+            get { return channel; }
+        }
+
+        public IEnumerable<PaasChannel> Channels {
+            get { return channels; }
+        }
+
+        public ChannelEventArgs (PaasChannel channel)
+        {
+            this.channel = channel;
+        }
+
+        public ChannelEventArgs (IEnumerable<PaasChannel> channels)
+        {
+            this.channels = channels;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/ChannelUpdateStatus.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/ChannelUpdateStatus.cs
new file mode 100644
index 0000000..031cb90
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/ChannelUpdateStatus.cs
@@ -0,0 +1,37 @@
+//
+// ChannelUpdateStatus.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.Aether
+{
+    public enum ChannelUpdateStatus
+    {
+        None,
+        Waiting,
+        Updating
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/ItemEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/ItemEventArgs.cs
new file mode 100644
index 0000000..d70fe0f
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/ItemEventArgs.cs
@@ -0,0 +1,57 @@
+//
+// ItemEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Aether
+{
+    public class ItemEventArgs : EventArgs
+    {
+        private readonly PaasItem item;
+        private readonly IEnumerable<PaasItem> items;
+
+        public PaasItem Item {
+            get { return item; }
+        }
+
+        public IEnumerable<PaasItem> Items {
+            get { return items; }
+        }
+
+        public ItemEventArgs (PaasItem item)
+        {
+            this.item = item;
+        }
+
+        public ItemEventArgs (IEnumerable<PaasItem> items)
+        {
+            this.items = items;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/GetCategoriesCompletedEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/GetCategoriesCompletedEventArgs.cs
new file mode 100644
index 0000000..04fb5de
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/GetCategoriesCompletedEventArgs.cs
@@ -0,0 +1,47 @@
+//
+// GetCategoriesCompletedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public class GetCategoriesCompletedEventArgs : RequestCompletedEventArgs
+    {
+        private readonly IEnumerable<MiroGuideCategoryInfo> categories;
+
+        public IEnumerable<MiroGuideCategoryInfo> Categories {
+            get { return categories; }
+        }
+
+        public GetCategoriesCompletedEventArgs (IEnumerable<MiroGuideCategoryInfo> categories,
+                                                Exception error, bool cancelled, bool timedout, object userState
+                                                ) : base (error, cancelled, MiroGuideClientMethod.GetChannels, timedout, userState)
+        {
+            this.categories = categories;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/GetChannelsEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/GetChannelsEventArgs.cs
new file mode 100644
index 0000000..7d7a6b2
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/GetChannelsEventArgs.cs
@@ -0,0 +1,54 @@
+//
+// GetChannelsCompletedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public class GetChannelsCompletedEventArgs : RequestCompletedEventArgs
+    {
+        private readonly SearchContext context;
+        private readonly IEnumerable<MiroGuideChannelInfo> channels;
+
+        public SearchContext Context {
+            get { return context; }
+        }
+
+        public IEnumerable<MiroGuideChannelInfo> Channels {
+            get { return channels; }
+        }
+
+        public GetChannelsCompletedEventArgs (SearchContext context,
+                                              IEnumerable<MiroGuideChannelInfo> channels,
+                                              Exception error, bool cancelled, bool timedout, object userState
+                                              ) : base (error, cancelled, MiroGuideClientMethod.GetChannels, timedout, userState)
+        {
+            this.context = context;
+            this.channels = channels;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideAccountInfo.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideAccountInfo.cs
new file mode 100644
index 0000000..e26536b
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideAccountInfo.cs
@@ -0,0 +1,107 @@
+//
+// MiroGuideAccountInfo.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public class MiroGuideAccountInfo
+    {
+        private string username, password_hash, service_uri, session_id;
+
+        public EventHandler<EventArgs> Updated;
+
+        public string SessionID {
+            get { return session_id; }
+
+            set {
+                if (!String.IsNullOrEmpty (value) && value != session_id) {
+                    session_id = value;
+                    Notify ();
+                }
+            }
+        }
+
+        public string PasswordHash {
+            get { return password_hash; }
+
+            set {
+                if (!String.IsNullOrEmpty (value) && value != password_hash) {
+                    password_hash = value;
+                    Notify ();
+                }
+            }
+        }
+
+        public string ServiceUri {
+            get { return service_uri; }
+
+            set {
+                if (!String.IsNullOrEmpty (value) && value != service_uri) {
+                    service_uri = value;
+                    Notify ();
+                }
+            }
+        }
+
+        public string Username {
+            get { return username; }
+
+            set {
+                if (!String.IsNullOrEmpty (value) && value != username) {
+                    username = value;
+                    Notify ();
+                }
+            }
+        }
+
+        public MiroGuideAccountInfo (string serviceUri,
+                                     string sessionID,
+                                     string username,
+                                     string passwordHash)
+        {
+            session_id = sessionID;
+
+            password_hash = passwordHash;
+            this.username = username;
+            service_uri = serviceUri;
+        }
+
+        public void Notify ()
+        {
+            OnUpdated ();
+        }
+
+        protected virtual void OnUpdated ()
+        {
+            var handler = Updated;
+
+            if (handler != null) {
+                handler (this, EventArgs.Empty);
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideCategoryInfo.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideCategoryInfo.cs
new file mode 100644
index 0000000..cbf38b8
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideCategoryInfo.cs
@@ -0,0 +1,51 @@
+//
+// MiroGuideCategoryInfo.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Hyena.Json;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public class MiroGuideCategoryInfo
+    {
+        private JsonObject category_info;
+
+        public MiroGuideCategoryInfo (JsonObject categoryInfo)
+        {
+            category_info = categoryInfo;
+        }
+
+        public string Name {
+            get { return category_info["name"].ToString (); }
+        }
+
+        public string Url {
+            get { return category_info["url"].ToString (); }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideChannelInfo.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideChannelInfo.cs
new file mode 100644
index 0000000..46436e3
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideChannelInfo.cs
@@ -0,0 +1,111 @@
+//
+// MiroGuideChannelInfo.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Hyena.Json;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public class MiroGuideChannelInfo
+    {
+        private JsonObject channel_info;
+
+        public MiroGuideChannelInfo (JsonObject channelInfo)
+        {
+            channel_info = channelInfo;
+        }
+
+        public long ID {
+            get { return Int32.Parse (channel_info["id"].ToString ()); }
+        }
+
+        public IEnumerable<string> Categories {
+            get {
+                if (channel_info.ContainsKey ("category")) {
+                    foreach (string j in channel_info["category"] as JsonArray) {
+                        if (j != null) {
+                            yield return j;
+                        }
+                    }
+                }
+            }
+        }
+
+        public string Description {
+            get { return channel_info["description"].ToString (); }
+        }
+
+        public bool IsHD {
+            get { return Boolean.Parse (channel_info["hi_def"].ToString ()); }
+        }
+
+        public IEnumerable<string> Languages {
+            get {
+                if (channel_info.ContainsKey ("language")) {
+                    foreach (string j in channel_info["language"] as JsonArray) {
+                        if (j != null) {
+                            yield return j;
+                        }
+                    }
+                }
+            }
+        }
+
+        public string Name {
+            get { return channel_info["name"].ToString (); }
+        }
+
+        public string Publisher {
+            get { return channel_info["publisher"].ToString (); }
+        }
+
+        public IEnumerable<string> Tags {
+            get {
+                if (channel_info.ContainsKey ("tag")) {
+                    foreach (string j in channel_info["tag"] as JsonArray) {
+                        if (j != null) {
+                            yield return j;
+                        }
+                    }
+                }
+            }
+        }
+
+        public string ThumbUrl {                                           // HACK UNTIL FIX'd BY MG GUYS!!!
+            get { return channel_info["thumbnail_url"].ToString ().Replace ("/370x247/", "/original/"); }
+        }
+
+        public string Url {
+            get { return channel_info["url"].ToString (); }
+        }
+
+        public string WebsiteUrl {
+            get { return channel_info["website_url"].ToString (); }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClient.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClient.cs
new file mode 100644
index 0000000..7c5259f
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClient.cs
@@ -0,0 +1,750 @@
+//
+// MiroGuideClient.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// This and AetherRequest are still a bit of an experiment.  This should be
+// implemented as a more formal state machine.  This will not be used by users for sometime.
+
+using System;
+using System.IO;
+using System.Net;
+using System.Web;
+
+using System.Linq;
+using System.Text;
+
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+
+using Hyena;
+using Hyena.Json;
+
+using Migo2.Async;
+
+using Banshee.ServiceStack;
+
+using Banshee.Base;
+using Banshee.Paas.Data;
+using Banshee.Paas.Aether;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public sealed class MiroGuideClient : AetherClient
+    {
+        private MiroGuideAccountInfo account;
+
+        private string session_id;
+        private Uri aether_service_uri;
+
+        private AsyncStateManager asm;
+
+        private AetherRequest request;
+
+        public EventHandler<RequestCompletedEventArgs> Completed;
+
+        public EventHandler<GetChannelsCompletedEventArgs> GetChannelsCompleted;
+        public EventHandler<GetCategoriesCompletedEventArgs> GetCategoriesCompleted;
+
+        public EventHandler<SubscriptionRequestedEventArgs> SubscriptionRequested;
+
+        public MiroGuideAccountInfo Account {
+            get { return Account; }
+        }
+
+        public ICredentials Credentials { get; set; }
+
+        public string ServiceUri {
+            get { return aether_service_uri.AbsoluteUri; }
+
+            set {
+                Uri tmp = new Uri (value.Trim ().TrimEnd ('/'));
+
+                if (tmp.Scheme != "http" && tmp.Scheme != "https") {
+                    throw new UriFormatException ("Scheme must be either http or https.");
+                }
+
+                aether_service_uri = tmp;
+            }
+        }
+
+        public string SessionID {
+            get {
+                return session_id;
+            }
+        }
+
+        public string UserAgent { get; set; }
+
+        public MiroGuideClient (MiroGuideAccountInfo account)
+        {
+            if (account == null) {
+                throw new ArgumentNullException ("account");
+            }
+
+            asm = new AsyncStateManager ();
+
+            this.account = account;
+            session_id = account.SessionID;
+
+            this.account.Updated += (sender, e) => {
+                lock (SyncRoot) {
+                    if (!String.IsNullOrEmpty (account.ServiceUri)) {
+                        ServiceUri = account.ServiceUri;
+                    }
+
+                    session_id = account.SessionID;
+                }
+            };
+
+            if (!String.IsNullOrEmpty (account.ServiceUri)) {
+                ServiceUri = account.ServiceUri;
+            }
+        }
+
+        public void CancelAsync ()
+        {
+            lock (SyncRoot) {
+                if (asm.Busy && asm.SetCancelled ()) {
+                    if (request != null) {
+                        request.CancelAsync ();
+                    }
+                }
+            }
+        }
+
+        public void GetCategoriesAsync ()
+        {
+            GetCategoriesAsync (null);
+        }
+
+        public void GetCategoriesAsync (object userState)
+        {
+            lock (SyncRoot) {
+                if (asm.Busy) {
+                    return;
+                }
+
+                NameValueCollection nvc = new NameValueCollection ();
+                nvc.Add ("datatype", "json");
+
+                BeginRequest (
+                    CreateGetRequestState (
+                        MiroGuideClientMethod.GetCategories, "/api/list_categories", nvc,
+                        ServiceFlags.None, null, userState
+                    ), true
+                );
+            }
+        }
+
+        public void GetChannelsAsync (MiroGuideFilterType filterType,
+                                      string filterValue,
+                                      MiroGuideSortType sortType,
+                                      bool reverse,
+                                      uint limit, uint offset)
+        {
+            GetChannelsAsync (filterType, filterValue, sortType, reverse, limit, offset, null);
+        }
+
+        public void GetChannelsAsync (MiroGuideFilterType filterType,
+                                      string filterValue,
+                                      MiroGuideSortType sortType,
+                                      bool reverse,
+                                      uint limit,
+                                      uint offset,
+                                      object userState)
+        {
+            if (String.IsNullOrEmpty (filterValue)) {
+                return;
+            }
+
+            GetChannelsAsync (new SearchContext (filterType, filterValue, sortType, reverse, limit, offset), userState);
+        }
+
+        public void GetChannelsAsync (SearchContext context)
+        {
+            GetChannelsAsync (context, null);
+        }
+
+        public void GetChannelsAsync (SearchContext context, object userState)
+        {
+            if (context == null) {
+                throw new ArgumentNullException ("context");
+            }
+
+            NameValueCollection nvc = new NameValueCollection ();
+
+            nvc.Add ("datatype", "json");
+
+            nvc.Add ("filter", ToQueryPart (context.FilterType));
+            nvc.Add ("filter_value", context.FilterValue);
+
+            if (context.SortType != MiroGuideSortType.Relevance) {
+                nvc.Add (
+                    "sort", String.Format ("{0}{1}", ((context.Reverse) ? "-" : String.Empty),
+                    ToQueryPart (context.SortType))
+                );
+            }
+
+            nvc.Add ("limit", context.Limit.ToString ());
+            nvc.Add ("offset", (context.Offset+context.Count).ToString ());
+
+            lock (SyncRoot) {
+                if (asm.Busy) { // Remove!!!  Allow requests to be queued in the future.
+                    return;
+                }
+
+                BeginRequest (
+                    CreateGetRequestState (
+                        MiroGuideClientMethod.GetChannels, "/api/get_channels", nvc,
+                        ServiceFlags.None, context, userState
+                    ), true
+                );
+            }
+        }
+
+        public void RequestSubsubscription (Uri uri)
+        {
+            if (uri == null) {
+                throw new ArgumentNullException ("uri");
+            }
+
+            OnSubscriptionRequested (uri);
+        }
+
+        public void RequestSubsubscription (IEnumerable<Uri> uris)
+        {
+            if (uris == null) {
+                throw new ArgumentNullException ("uris");
+            }
+
+            OnSubscriptionRequested (uris);
+        }
+
+        private void GetSessionAsync (MiroGuideRequestState callingMethodState)
+        {
+            if (callingMethodState == null) {
+                throw new ArgumentNullException ("callingMethodState");
+            }
+
+            NameValueCollection nvc = new NameValueCollection ();
+            nvc.Add ("datatype", "json");
+
+            lock (SyncRoot) {
+                BeginRequest (
+                    CreateGetRequestState (
+                        MiroGuideClientMethod.GetSession, "/api/get_session", nvc,
+                        ServiceFlags.None, null, null, callingMethodState
+                    ), false
+                );
+            }
+        }
+
+        public void AddSubscriptionsAsync (IEnumerable<string> urls)
+        {
+            AddSubscriptionsAsync (urls, null);
+        }
+
+        public void AddSubscriptionsAsync (IEnumerable<string> urls, object userState)
+        {
+            ManageSubscriptionsAsync (MiroGuideClientMethod.AddSubscriptions, urls, userState);
+        }
+
+        public void DeleteSubscriptionsAsync (IEnumerable<string> urls)
+        {
+                DeleteSubscriptionsAsync (urls, null);
+        }
+
+        public void DeleteSubscriptionsAsync (IEnumerable<string> urls, object userState)
+        {
+            ManageSubscriptionsAsync (MiroGuideClientMethod.DelSubscriptions, urls, userState);
+        }
+
+        private void ManageSubscriptionsAsync (MiroGuideClientMethod method, IEnumerable<string> urls, object userState)
+        {
+            lock (SyncRoot) {
+                if (asm.Busy) {
+                    return;
+                }
+
+                string fragment;
+
+                if (method == MiroGuideClientMethod.AddSubscriptions) {
+                    fragment = "/api/add_subscriptions";
+                } else {
+                    fragment = "/api/del_subscriptions";
+                }
+
+                JsonObject json_data = new JsonObject ();
+                JsonArray request_data = new JsonArray ();
+
+                request_data.AddRange (urls.Select (u => HttpUtility.UrlEncode (u)).Cast<object>());
+                json_data["urls"] = request_data;
+
+                BeginRequest (
+                    CreatePostRequestState (
+                        method, fragment,
+                        "application/x-www-form-urlencoded", null,
+                        String.Format ("urls={0}", SerializeJson (json_data)),
+                        ServiceFlags.RequireAuth, null, null
+                    ), true
+                );
+            }
+        }
+
+        public void GetSubscriptionsAsync ()
+        {
+            GetSubscriptionsAsync (null);
+        }
+
+        public void GetSubscriptionsAsync (object userState)
+        {
+            lock (SyncRoot) {
+                if (asm.Busy) {
+                    return;
+                }
+
+                NameValueCollection nvc = new NameValueCollection ();
+                nvc.Add ("datatype", "json");
+
+                BeginRequest (
+                    CreateGetRequestState (
+                        MiroGuideClientMethod.GetSubscriptions, "/api/get_subscriptions", nvc,
+                        ServiceFlags.RequireAuth, null, userState
+                    ), true
+                );
+            }
+        }
+
+        private AetherRequest CreateRequest ()
+        {
+            AetherRequest req = new AetherRequest () {
+                Timeout = (30 * 1000),
+                Credentials = Credentials,
+                UserAgent = UserAgent
+            };
+
+            req.Completed += OnRequestCompletedHandler;
+
+            return req;
+        }
+
+        private void BeginRequest (MiroGuideRequestState state, bool changeState)
+        {
+            if (changeState) {
+                asm.SetBusy ();
+                OnStateChanged (AetherClientState.Idle, AetherClientState.Busy);
+            }
+
+            if (asm.Cancelled) {
+                state = GetHead (state);
+                state.Cancelled = true;
+                Complete (state);
+                return;
+            }
+
+            try {
+                if (state.ServiceFlags != ServiceFlags.None) {
+                    if ((state.ServiceFlags & ServiceFlags.RequireAuth) != 0) {
+                        if (String.IsNullOrEmpty (SessionID)) {
+                            GetSessionAsync (state);
+                            return;
+                        } else {
+                            state.AddParameter ("session", SessionID);
+                        }
+                    }
+                }
+
+                request = CreateRequest ();
+
+                switch (state.HttpMethod) {
+                case HttpMethod.GET:
+                    request.BeginGetRequest (state.GetFullUri (), state);
+                    break;
+                case HttpMethod.POST:
+                    request.ContentType = state.ContentType;
+                    request.BeginPostRequest (
+                        state.GetFullUri (), Encoding.UTF8.GetBytes (state.RequestData), state
+                    );
+                    break;
+                }
+            } catch (Exception e) {
+                state = GetHead (state);
+                state.Error = e;
+                Hyena.Log.Exception (e);
+                Complete (state);
+            }
+        }
+
+        private void Complete (MiroGuideRequestState state)
+        {
+            try {
+                switch (state.Method) {
+                case MiroGuideClientMethod.GetSession:
+                    HandleGetSessionResponse (state.ResponseData);
+                    break;
+                }
+            } catch (Exception e) {
+                state = GetHead (state);
+                state.Error = e;
+                Hyena.Log.Exception (e);
+            }
+
+            if (state.CallingState != null) {
+                BeginRequest (state.CallingState, false);
+                return;
+            } else {
+                switch (state.Method) {
+                case MiroGuideClientMethod.GetChannels:
+                    HandleGetChannelsResponse (state);
+                    break;
+                case MiroGuideClientMethod.GetCategories:
+                    HandleGetCategoriesResponse (state);
+                    break;
+                case MiroGuideClientMethod.GetSubscriptions:
+                    HandleGetSubscriptionsResponse (state);
+                    break;
+                }
+
+                asm.Reset ();
+                OnStateChanged (AetherClientState.Busy, AetherClientState.Idle);
+                OnCompleted (state);
+            }
+        }
+
+        private MiroGuideRequestState CreateGetRequestState (MiroGuideClientMethod acm,
+                                                             string path,
+                                                             NameValueCollection parameters,
+                                                             ServiceFlags flags,
+                                                             object internalState,
+                                                             object userState)
+        {
+            return CreateGetRequestState (
+                acm, path, parameters, flags, internalState, userState, null
+            );
+        }
+
+        private MiroGuideRequestState CreateGetRequestState (MiroGuideClientMethod acm,
+                                                             string path,
+                                                             NameValueCollection parameters,
+                                                             ServiceFlags flags,
+                                                             object internalState,
+                                                             object userState,
+                                                             MiroGuideRequestState callingState)
+        {
+            return CreateRequestState (
+                acm, path, HttpMethod.GET, null, parameters, null,
+                flags, internalState, userState, callingState
+            );
+        }
+
+        private MiroGuideRequestState CreatePostRequestState (MiroGuideClientMethod acm,
+                                                              string path,
+                                                              string contentType,
+                                                              NameValueCollection parameters,
+                                                              string requestData,
+                                                              ServiceFlags flags,
+                                                              object internalState,
+                                                              object userState)
+        {
+            return CreatePostRequestState (
+                acm, path, contentType, parameters, requestData,
+                flags, internalState, userState, null
+            );
+        }
+
+        private MiroGuideRequestState CreatePostRequestState (MiroGuideClientMethod acm,
+                                                              string path,
+                                                              string contentType,
+                                                              NameValueCollection parameters,
+                                                              string requestData,
+                                                              ServiceFlags flags,
+                                                              object internalState,
+                                                              object userState,
+                                                              MiroGuideRequestState callingState)
+        {
+            return CreateRequestState (
+                acm, path, HttpMethod.POST, contentType, parameters, requestData,
+                flags, internalState, userState, callingState
+            );
+        }
+
+        private MiroGuideRequestState CreateRequestState (MiroGuideClientMethod acm,
+                                                          string path,
+                                                          HttpMethod method,
+                                                          string contentType,
+                                                          NameValueCollection parameters,
+                                                          string requestData,
+                                                          ServiceFlags flags,
+                                                          object internalState,
+                                                          object userState,
+                                                          MiroGuideRequestState callingState)
+        {
+            MiroGuideRequestState state = new MiroGuideRequestState () {
+                Method = acm,
+                RequestData = requestData,
+                HttpMethod = method,
+                ContentType = contentType,
+                ServiceFlags = flags,
+                UserState = userState,
+                InternalState = internalState,
+                CallingState = callingState,
+                BaseUri = ServiceUri+path
+            };
+
+            if (parameters != null) {
+                state.AddParameters (parameters);
+            }
+
+            return state;
+        }
+
+        private MiroGuideRequestState GetHead (MiroGuideRequestState state)
+        {
+            while (state.CallingState != null) {
+                state = state.CallingState;
+            }
+
+            return state;
+        }
+
+        private object DeserializeJson (string response)
+        {
+            Deserializer d = new Deserializer ();
+            d.SetInput (response);
+            return d.Deserialize ();
+        }
+
+        private string SerializeJson (object json)
+        {
+            return new Serializer (json).Serialize ();
+        }
+
+        private void HandleGetSessionResponse (string response)
+        {
+            object session;
+            JsonObject resp = DeserializeJson (response) as JsonObject;
+
+            if (resp.TryGetValue ("session", out session) && !String.IsNullOrEmpty (session as string)) {
+                session_id = session as string;
+                account.SessionID = session_id;
+
+                ThreadAssist.ProxyToMain (delegate {
+                    Banshee.Web.Browser.Open (
+                        account.ServiceUri+String.Format ("/api/authenticate?session={0}", session)
+                    );
+                });
+            } else {
+                throw new Exception ("Response did not contain session id");
+            }
+        }
+
+        private void HandleGetCategoriesResponse (MiroGuideRequestState state)
+        {
+            List<MiroGuideCategoryInfo> categories = null;
+
+            try {
+                if (state.Succeeded) {
+                    categories = new List<MiroGuideCategoryInfo> ();
+
+                    foreach (JsonObject o in DeserializeJson (state.ResponseData) as JsonArray) {
+                        try {
+                            categories.Add (new MiroGuideCategoryInfo (o));
+                        } catch { continue; }
+                    }
+                }
+            } catch (Exception e) {
+                state.Error = e;
+            } finally {
+                OnGetCategoriesCompleted (state, categories);
+            }
+        }
+
+        private void HandleGetChannelsResponse (MiroGuideRequestState state)
+        {
+            List<MiroGuideChannelInfo> channels = new List<MiroGuideChannelInfo> ();
+
+            try {
+                if (state.Succeeded) {
+                    foreach (JsonObject o in DeserializeJson (state.ResponseData) as JsonArray) {
+                        try {
+                            channels.Add (new MiroGuideChannelInfo (o));
+                        } catch { continue; }
+                    }
+
+                    SearchContext context = state.InternalState as SearchContext;
+                    context.IncrementResultCount ((uint)channels.Count);
+                }
+            } catch (Exception e) {
+                state.Error = e;
+            } finally {
+                OnGetChannelsCompleted (state, channels);
+            }
+        }
+
+        private void HandleGetSubscriptionsResponse (MiroGuideRequestState state)
+        {
+            List<Uri> urls = null;
+
+            try {
+                if (state.Succeeded) {
+                    urls = new List<Uri> ();
+                    JsonArray ary = (DeserializeJson (state.ResponseData) as JsonObject)["urls"] as JsonArray;
+
+                    foreach (var o in ary) {
+                        try {
+                            urls.Add (new Uri (o.ToString ()));
+                        } catch { continue; }
+                    }
+
+                    if (urls.Count > 0) {
+                        RequestSubsubscription (urls);
+                    }
+                }
+            } catch (Exception e) {
+                state.Error = e;
+            }
+        }
+
+        private void OnRequestCompletedHandler (object sender, AetherRequestCompletedEventArgs e)
+        {
+            lock (SyncRoot) {
+                request.Completed -= OnRequestCompletedHandler;
+                MiroGuideRequestState state = e.UserState as MiroGuideRequestState;
+
+                state.Completed = true;
+                state.ResponseData = (e.Data != null) ? Encoding.UTF8.GetString (e.Data) : String.Empty;
+
+                if (e.Cancelled || asm.Cancelled) {
+                    state = GetHead (state);
+                    state.Cancelled = true;
+                } else if (e.Timedout) {
+                    state = GetHead (state);
+                    state.Timedout = true;
+                } else if (e.Error != null) {
+                    state = GetHead (state);
+                    state.Error = e.Error;
+                    Hyena.Log.Exception (e.Error);
+                }
+
+                Complete (state);
+            }
+        }
+
+        private void OnCompleted (MiroGuideRequestState state)
+        {
+            var handler = Completed;
+
+            RequestCompletedEventArgs e = new RequestCompletedEventArgs (
+                state.Error, state.Cancelled, state.Method, state.Timedout, state.UserState
+            );
+
+            if (handler != null) {
+                EventQueue.Register (new EventWrapper<RequestCompletedEventArgs> (handler, this, e));
+            }
+        }
+
+        private void OnGetCategoriesCompleted (MiroGuideRequestState state, IEnumerable<MiroGuideCategoryInfo> categories)
+        {
+            var handler = GetCategoriesCompleted;
+
+            GetCategoriesCompletedEventArgs e = new GetCategoriesCompletedEventArgs (
+                categories, state.Error, state.Cancelled, state.Timedout, state.UserState
+            );
+
+            if (handler != null) {
+                EventQueue.Register (new EventWrapper<GetCategoriesCompletedEventArgs> (handler, this, e));
+            }
+        }
+
+        private void OnGetChannelsCompleted (MiroGuideRequestState state, IEnumerable<MiroGuideChannelInfo> channels)
+        {
+            var handler = GetChannelsCompleted;
+
+            GetChannelsCompletedEventArgs e = new GetChannelsCompletedEventArgs (
+                state.InternalState as SearchContext, channels,
+                state.Error, state.Cancelled, state.Timedout, state.UserState
+            );
+
+            if (handler != null) {
+                EventQueue.Register (new EventWrapper<GetChannelsCompletedEventArgs> (handler, this, e));
+            }
+        }
+
+        private void OnSubscriptionRequested (Uri uri)
+        {
+            OnSubscriptionRequested (new SubscriptionRequestedEventArgs (uri));
+        }
+
+        private void OnSubscriptionRequested (IEnumerable<Uri> uris)
+        {
+            OnSubscriptionRequested (new SubscriptionRequestedEventArgs (uris));
+        }
+
+        private void OnSubscriptionRequested (SubscriptionRequestedEventArgs e)
+        {
+            var handler = SubscriptionRequested;
+
+            if (handler != null) {
+                EventQueue.Register (
+                    new EventWrapper<SubscriptionRequestedEventArgs> (handler, this, e)
+                );
+            }
+        }
+
+        private string ToQueryPart (MiroGuideFilterType type)
+        {
+            switch (type)
+            {
+            case MiroGuideFilterType.Category:  return "category";
+            case MiroGuideFilterType.Language:  return "language";
+            case MiroGuideFilterType.Name:      return "name";
+            case MiroGuideFilterType.Search:    return "search";
+            case MiroGuideFilterType.Tag:       return "tag";
+            case MiroGuideFilterType.HD:        return "hd";
+            case MiroGuideFilterType.Featured:  return "featured";
+            case MiroGuideFilterType.TopRated:  return "feed";
+            case MiroGuideFilterType.Popular:   goto case MiroGuideFilterType.TopRated;
+            default:
+                goto case MiroGuideFilterType.Search;
+            }
+        }
+
+        private string ToQueryPart (MiroGuideSortType type)
+        {
+            switch (type)
+            {
+            case MiroGuideSortType.Age:       return "age";
+            case MiroGuideSortType.ID:        return "id";
+            case MiroGuideSortType.Name:      return "name";
+            case MiroGuideSortType.Popular:   return "popular";
+            case MiroGuideSortType.Rating:    return "rating";
+            case MiroGuideSortType.Relevance: return String.Empty;
+            default:
+                goto case MiroGuideSortType.Name;
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClientError.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClientError.cs
new file mode 100644
index 0000000..8c25ddc
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClientError.cs
@@ -0,0 +1,36 @@
+//
+// MiroGuideClientError.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public enum MiroGuideClientError
+    {
+        ConnectionError,
+        InvalidCredentials
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClientMethod.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClientMethod.cs
new file mode 100644
index 0000000..feb0df2
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClientMethod.cs
@@ -0,0 +1,37 @@
+//
+// MiroGuideClientMethod.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public enum MiroGuideClientMethod {
+        GetChannels,
+        GetSession,
+        GetCategories,
+        AddSubscriptions,
+        DelSubscriptions,
+        GetSubscriptions
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideFilterType.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideFilterType.cs
new file mode 100644
index 0000000..97d7403
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideFilterType.cs
@@ -0,0 +1,44 @@
+//
+// MiroGuideFilterType.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public enum MiroGuideFilterType : int
+    {
+        None,
+        Category,
+        Language,
+        Name,
+        Search,
+        Tag,
+        HD,
+        Featured,
+        TopRated,
+        Popular,
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideMethodCompletedEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideMethodCompletedEventArgs.cs
new file mode 100644
index 0000000..6037219
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideMethodCompletedEventArgs.cs
@@ -0,0 +1,46 @@
+//
+// MiroGuideMethodCompletedEventArgs.cs - Way too fucking long.
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.ComponentModel;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public class MiroGuideMethodCompletedEventArgs : AsyncCompletedEventArgs
+    {
+        private readonly bool timedout;
+
+        public bool Timedout {
+            get { return timedout; }
+        }
+
+        public MiroGuideMethodCompletedEventArgs (Exception err, bool cancelled, bool timedout, object userState)
+            : base (err, cancelled, userState)
+        {
+            this.timedout = timedout;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideRequestState.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideRequestState.cs
new file mode 100644
index 0000000..0b8abf3
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideRequestState.cs
@@ -0,0 +1,132 @@
+//
+// MiroGuideRequestState.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Text;
+
+using System.Collections.Generic;
+using System.Collections.Specialized;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    class MiroGuideRequestState
+    {
+        private Dictionary<string, string> parameters;
+
+        public bool Timedout { get; set; }
+        public bool Cancelled { get; set; }
+        public bool Completed { get; set; }
+
+        public string BaseUri { get; set; }
+        public Exception Error { get; set; }
+
+        public string ContentType { get; set; }
+        public HttpMethod HttpMethod { get; set; }
+
+        public MiroGuideClientMethod Method { get; set; }
+
+        public string RequestData { get; set; }
+        public string ResponseData { get; set; }
+
+        public ServiceFlags ServiceFlags { get; set; }
+
+        public object UserState { get; set; }
+        public object InternalState { get; set; }
+
+        public MiroGuideRequestState CallingState { get; set; }
+
+        public bool Succeeded {
+            get { return Completed & !Timedout & !Cancelled & (Error == null); }
+        }
+
+        public MiroGuideRequestState ()
+        {
+            parameters = new Dictionary<string, string> ();
+        }
+
+        public Uri GetFullUri ()
+        {
+            return new Uri (String.Format ("{0}{1}", BaseUri, GetParameterString ()));
+        }
+
+        public void AddParameter (string key, string val)
+        {
+            if (parameters.ContainsKey (key)) {
+                parameters[key] = val;
+            } else {
+                parameters.Add (key, val);
+            }
+        }
+
+        public void AddParameters (NameValueCollection nvc)
+        {
+            if (nvc != null) {
+                foreach (string key in nvc) {
+                    AddParameter (key, nvc[key]);
+                }
+            }
+        }
+
+        public void ClearParameters ()
+        {
+            parameters.Clear ();
+        }
+
+        public void RemoveParameter (string key)
+        {
+            if (parameters.ContainsKey (key)) {
+                parameters.Remove (key);
+            }
+        }
+
+        private string GetParameterString ()
+        {
+            if (parameters.Count == 0) {
+                return String.Empty;
+            }
+
+            bool first = true;
+            StringBuilder sb = new StringBuilder ();
+
+            sb.Append ('?');
+
+            foreach (KeyValuePair<string, string> kvp in parameters) {
+                if (!first) {
+                    sb.Append ('&');
+                }
+
+                sb.AppendFormat ("{0}={1}",
+                    System.Web.HttpUtility.UrlEncode (kvp.Key),
+                    System.Web.HttpUtility.UrlEncode (kvp.Value)
+                );
+
+                first = false;
+            }
+
+            return sb.ToString ();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideSortType.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideSortType.cs
new file mode 100644
index 0000000..d072625
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideSortType.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace Banshee.Paas
+{
+    public enum MiroGuideSortType
+    {
+        None      = 0,
+        Age       = 1,
+        ID        = 2,
+        Name      = 3,
+        Popular   = 4,
+        Rating    = 5,
+        Relevance = 6
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/RequestCompletedEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/RequestCompletedEventArgs.cs
new file mode 100644
index 0000000..0851925
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/RequestCompletedEventArgs.cs
@@ -0,0 +1,55 @@
+//
+// RequestCompletedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.ComponentModel;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public class RequestCompletedEventArgs : AsyncCompletedEventArgs
+    {
+        private readonly MiroGuideClientMethod method;
+        private readonly bool timedout;
+
+        public MiroGuideClientMethod Method {
+            get { return method; }
+        }
+
+        public bool Timedout {
+            get { return timedout; }
+        }
+
+        public RequestCompletedEventArgs (Exception err,
+                                          bool cancelled,
+                                          MiroGuideClientMethod method,
+                                          bool timedout,
+                                          object userState) : base (err, cancelled, userState)
+        {
+            this.method = method;
+            this.timedout = timedout;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/SearchContext.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/SearchContext.cs
new file mode 100644
index 0000000..35cfda9
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/SearchContext.cs
@@ -0,0 +1,126 @@
+//
+// SearchContext.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public class SearchContext
+    {
+        private int page;
+        private uint count;
+        private uint limit;
+        private uint offset;
+        private bool reverse;
+        private bool channels_available;
+
+        private MiroGuideSortType sort_type;
+        private readonly string filter_value;
+        private readonly MiroGuideFilterType filter_type;
+
+        public uint Count {
+            get { return count; }
+        }
+
+        public uint Limit {
+            get { return limit; }
+            set { limit = value; }
+        }
+
+        public int Page {
+            get { return page; }
+        }
+
+        public uint Offset {
+            get { return offset; }
+        }
+
+        public bool Reverse {
+            get { return reverse; }
+            set { reverse = value; }
+        }
+
+        public bool ChannelsAvailable {
+            get { return channels_available; }
+        }
+
+        public MiroGuideFilterType FilterType {
+            get { return filter_type; }
+        }
+
+        public string FilterValue {
+            get { return filter_value; }
+        }
+
+        public MiroGuideSortType SortType {
+            get { return sort_type; }
+            set { sort_type = value; }
+        }
+
+        public SearchContext (MiroGuideFilterType filterType,
+                              string filterValue,
+                              MiroGuideSortType sortType,
+                              bool reverse, uint limit, uint offset)
+        {
+            if (String.IsNullOrEmpty (filterValue)) {
+                return;
+            }
+
+            page = -1;
+            count = 0;
+
+            channels_available = true;
+
+            this.limit = limit;
+            this.offset = offset;
+            this.reverse = reverse;
+
+            sort_type = sortType;
+
+            filter_type = filterType;
+            filter_value = filterValue;
+        }
+
+        public void IncrementResultCount (uint results)
+        {
+            ++page;
+            count += results;
+
+            if (results < limit) {
+                channels_available = false;
+            }
+        }
+
+        public void Reset ()
+        {
+            page = -1;
+            count = 0;
+            offset = 0;
+
+            channels_available = true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/ServiceMethodFlags.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/ServiceMethodFlags.cs
new file mode 100644
index 0000000..b604419
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/ServiceMethodFlags.cs
@@ -0,0 +1,37 @@
+//
+// ServiceMethodFlags.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    [Flags]
+    enum ServiceFlags
+    {
+        None            = 0x00,
+        RequireAuth     = 0x01
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/SubscriptionRequestedEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/SubscriptionRequestedEventArgs.cs
new file mode 100644
index 0000000..2d568a1
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/SubscriptionRequestedEventArgs.cs
@@ -0,0 +1,55 @@
+//
+// SubscriptionRequestedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+namespace Banshee.Paas.Aether.MiroGuide
+{
+    public class SubscriptionRequestedEventArgs : EventArgs
+    {
+        private readonly Uri uri;
+        private readonly IEnumerable<Uri> uris;
+
+        public Uri Uri {
+            get { return uri; }
+        }
+
+        public IEnumerable<Uri> Uris {
+            get { return uris; }
+        }
+
+        public SubscriptionRequestedEventArgs (Uri uri)
+        {
+            this.uri = uri;
+        }
+
+        public SubscriptionRequestedEventArgs (IEnumerable<Uri> uris)
+        {
+            this.uris = uris;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/RequestState.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/RequestState.cs
new file mode 100644
index 0000000..c2a790f
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/RequestState.cs
@@ -0,0 +1,141 @@
+//
+// RequestState.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+
+// This needs to be added to Migo and used in the AsyncWebClient.
+
+namespace Banshee.Paas.Aether
+{
+    class RequestState : IDisposable
+    {
+        public const int BufferSize = 8192;
+
+        private AutoResetEvent timeout_handle;
+        private RegisteredWaitHandle registered_timeout_handle;
+
+        public byte[] ReadBuffer  { get; set; }
+        public byte[] WriteBuffer { get; set; }
+
+        public Exception Error { get; set; }
+
+        public HttpWebRequest  Request  { get; set; }
+        public HttpWebResponse Response { get; set; }
+
+        public MemoryStream ReadData { get; set; }
+
+        public Stream RequestStream   {get; set; }
+        public Stream ResponseStream  {get; set; }
+
+        public object UserState { get; set; }
+
+        public RequestState ()
+        {
+            ReadBuffer = new byte[BufferSize];
+            ReadData = new MemoryStream ();
+        }
+
+        public void Dispose ()
+        {
+            RemoveTimeout ();
+
+            if (ReadData != null) {
+                ReadData.Close ();
+                ReadData = null;
+            }
+
+            if (Response != null) {
+                Response.Close ();
+                Response = null;
+            }
+
+            if (RequestStream != null) {
+                RequestStream.Close ();
+                RequestStream = null;
+            }
+
+            if (ResponseStream != null) {
+                ResponseStream.Close ();
+                ResponseStream = null;
+            }
+
+            Request = null;
+            UserState = null;
+
+            ReadBuffer = null;
+            WriteBuffer = null;
+        }
+
+        public void AddTimeout (WaitOrTimerCallback callback, int timeout, bool executeOnlyOnce, object state)
+        {
+            if (timeout_handle != null) {
+                throw new InvalidOperationException ("Cannot nest timeouts.");
+            }
+
+            timeout_handle = new AutoResetEvent (false);
+
+            if (executeOnlyOnce) {
+                ThreadPool.RegisterWaitForSingleObject (
+                    timeout_handle, callback, state, timeout, true
+                );
+            } else {
+                registered_timeout_handle = ThreadPool.RegisterWaitForSingleObject (
+                    timeout_handle, callback, state, timeout, false
+                );
+            }
+        }
+
+        public void RemoveTimeout ()
+        {
+            RemoveTimeout (false);
+        }
+
+        public void RemoveTimeout (bool flag)
+        {
+            if (flag) {
+                SetTimeoutHandle ();
+            }
+
+            if (registered_timeout_handle != null) {
+                registered_timeout_handle.Unregister (null);
+                registered_timeout_handle = null;
+            }
+
+            timeout_handle = null;
+        }
+
+        public void SetTimeoutHandle ()
+        {
+            if (timeout_handle != null) {
+                timeout_handle.Set ();
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateCompletedEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateCompletedEventArgs.cs
new file mode 100644
index 0000000..2865dd8
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateCompletedEventArgs.cs
@@ -0,0 +1,53 @@
+//
+// ChannelUpdateCompletedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Aether.Syndication
+{
+    public class ChannelUpdateCompletedEventArgs : ChannelEventArgs
+    {
+        private readonly Exception err;
+        private readonly bool succeeded;
+
+        public Exception Error {
+            get { return err; }
+        }
+
+        public bool Succeeded {
+            get { return succeeded; }
+        }
+
+        public ChannelUpdateCompletedEventArgs (PaasChannel channel, bool succeeded, Exception err) : base (channel)
+        {
+            this.err = err;
+            this.succeeded = succeeded;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateManager.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateManager.cs
new file mode 100644
index 0000000..04f4ac5
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateManager.cs
@@ -0,0 +1,50 @@
+//
+// ChannelUpdateManager.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+using Migo2.Async;
+
+namespace Banshee.Paas.Aether.Syndication
+{
+    public class ChannelUpdateManager : TaskGroup<ChannelUpdateTask>
+    {
+        public ChannelUpdateManager (int maxConcurrentUpdates) : base (maxConcurrentUpdates)
+        {
+        }
+
+        public override void Dispose ()
+        {
+            if (SetDisposing ()) {
+                CancelAsync ();
+                Handle.WaitOne ();
+                base.Dispose ();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateTask.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateTask.cs
new file mode 100644
index 0000000..2edf306
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateTask.cs
@@ -0,0 +1,123 @@
+//
+// ChannelUpdateTask.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Migo2.Net;
+using Migo2.Async;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Aether.Syndication
+{
+    public class ChannelUpdateTask : Task
+    {
+        private AsyncWebClient wc;
+        private PaasChannel channel;
+        private AsyncStateManager state_manager;
+
+        private string result;
+
+        public PaasChannel Channel {
+            get { return channel; }
+        }
+
+        public string Result {
+            get { return result; }
+        }
+
+        public ChannelUpdateTask (PaasChannel channel) : base (null, channel)
+        {
+            this.channel = channel;
+            state_manager = new AsyncStateManager ();
+        }
+
+        public override void CancelAsync ()
+        {
+            lock (SyncRoot) {
+                if (state_manager.SetCancelled ()) {
+                    SetState (TaskState.Cancelled);
+
+                    if (wc == null) {
+                        EmitCompletionEvent (null);
+                    } else {
+                       wc.CancelAsync ();
+                    }
+                }
+            }
+        }
+
+        public override void ExecuteAsync ()
+        {
+            lock (SyncRoot) {
+                if (state_manager.SetBusy ()) {
+                    SetState (TaskState.Running);
+
+                    OnStarted ();
+
+                    try {
+                        wc = new AsyncWebClient ();
+                        wc.Timeout = (30 * 1000); // 30 Seconds
+
+                        wc.DownloadStringCompleted += OnDownloadDataReceived;
+                        wc.DownloadStringAsync (new Uri (channel.Url));
+                    } catch (Exception e) {
+                        EmitCompletionEvent (e);
+                    }
+                }
+            }
+        }
+
+        private void OnDownloadDataReceived (object sender, DownloadStringCompletedEventArgs args)
+        {
+            Exception error = null;
+
+            lock (SyncRoot) {
+                if (!state_manager.Cancelled) {
+                    state_manager.SetCompleted ();
+
+                    if (args.Error != null) {
+                        error = args.Error;
+                    } else {
+                        result = args.Result;
+                    }
+                }
+
+                EmitCompletionEvent (error);
+            }
+        }
+
+        private void EmitCompletionEvent (Exception e)
+        {
+            if (wc != null) {
+                wc.DownloadStringCompleted -= OnDownloadDataReceived;
+            }
+
+            OnTaskCompleted (e, state_manager.Cancelled);
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ItmsPodcast.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ItmsPodcast.cs
new file mode 100644
index 0000000..e2fa108
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ItmsPodcast.cs
@@ -0,0 +1,100 @@
+//
+// ItmsPodcast.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Text.RegularExpressions;
+
+using Banshee.Web;
+
+namespace Banshee.Paas.Aether.Syndication
+{
+    public class ItmsPodcast
+    {
+        private string itms_uri;
+        private string feed_url;
+        private string xml;
+
+        public ItmsPodcast (string itmsUri)
+        {
+            Fetch (itmsUri, 2);
+            feed_url = GetString ("feedURL");
+        }
+
+        public string FeedUrl {
+            get { return feed_url; }
+        }
+
+        private void Fetch (string url, int tries)
+        {
+            url = url.Replace ("itms://", "http://";);
+
+            // Get rid of all the url variables except the id
+            int args_start = url.IndexOf ("?");
+            Regex id_regex = new Regex ("[?&]id=(\\d+)", RegexOptions.IgnoreCase);
+            Match match = id_regex.Match (url, args_start);
+            url = String.Format ("{0}?id={1}",
+                url.Substring (0, args_start),
+                match.Groups[1]
+            );
+
+            using (HttpRequest req = new HttpRequest (url)) {
+                req.Request.KeepAlive = true;
+                req.Request.Accept = "*/*";
+                req.GetResponse ();
+
+                if (req.Response.ContentType.StartsWith ("text/html")) {
+                    if (tries > 0) {
+                        string start = "onload=\"return itmsOpen('";
+                        string rsp_body = req.ResponseBody;
+                        int value_start = rsp_body.IndexOf (start) + start.Length;
+                        int value_end   = rsp_body.IndexOf ("','", value_start);
+                        string new_url  = rsp_body.Substring (value_start, value_end - value_start);
+                        new_url = System.Web.HttpUtility.HtmlDecode (new_url);
+                        Fetch (new_url, tries--);
+                    }
+                } else {
+                    xml = req.ResponseBody;
+                    itms_uri = url;
+                }
+            }
+        }
+
+        private string GetString (string key_name)
+        {
+            try {
+                int entry_start = xml.IndexOf (String.Format ("<key>{0}</key>", key_name));
+                int value_start = xml.IndexOf ("<string>", entry_start) + 8;
+                int value_end   = xml.IndexOf ("</string>", value_start);
+                return xml.Substring (value_start, value_end - value_start);
+            } catch (Exception) {
+                throw new Exception (String.Format (
+                    "Unable to get value for {0} from {1}", key_name, itms_uri));
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/RssParser.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/RssParser.cs
new file mode 100644
index 0000000..97b6da6
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/RssParser.cs
@@ -0,0 +1,286 @@
+//
+// RssParser.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2007 Mike Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Xml;
+using System.Text;
+using System.Collections.Generic;
+
+using Hyena;
+
+using Migo2.Utils;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Aether.Syndication
+{
+    public class RssParser
+    {
+        private XmlDocument doc;
+        private XmlNamespaceManager mgr;
+
+        public RssParser (string xml)
+        {
+            xml = xml.TrimStart ();
+            doc = new XmlDocument ();
+            try {
+                doc.LoadXml (xml);
+            } catch (XmlException e) {
+                bool have_stripped_control = false;
+                StringBuilder sb = new StringBuilder ();
+
+                foreach (char c in xml) {
+                    if (Char.IsControl (c) && c != '\n') {
+                        have_stripped_control = true;
+                    } else {
+                        sb.Append (c);
+                    }
+                }
+
+                bool loaded = false;
+                if (have_stripped_control) {
+                    try {
+                        doc.LoadXml (sb.ToString ());
+                        loaded = true;
+                    } catch {}
+                }
+
+                if (!loaded) {
+                    Hyena.Log.Exception (e);
+                    throw new FormatException ("Invalid XML document.");
+                }
+            }
+            CheckRss ();
+        }
+
+        public RssParser (XmlDocument doc)
+        {
+            this.doc = doc;
+            CheckRss ();
+        }
+
+        public void UpdateChannel (PaasChannel channel)
+        {
+            try {
+                channel.Name          = StringUtil.RemoveNewlines (XmlUtils.GetXmlNodeText (doc, "/rss/channel/title", mgr));
+                channel.Description   = StringUtil.RemoveNewlines (XmlUtils.GetXmlNodeText (doc, "/rss/channel/description", mgr));
+                channel.Copyright     = XmlUtils.GetXmlNodeText (doc, "/rss/channel/copyright", mgr);
+                channel.ImageUrl      = XmlUtils.GetXmlNodeText (doc, "/rss/channel/itunes:image/@href", mgr);
+
+                if (String.IsNullOrEmpty (channel.ImageUrl)) {
+                    channel.ImageUrl = XmlUtils.GetXmlNodeText (doc, "/rss/channel/image/url", mgr);
+                }
+
+                channel.Language      = XmlUtils.GetXmlNodeText (doc, "/rss/channel/language", mgr);
+                channel.LastBuildDate = XmlUtils.GetRfc822DateTime (doc, "/rss/channel/lastBuildDate");
+                channel.Link          = XmlUtils.GetXmlNodeText (doc, "/rss/channel/link", mgr);
+                channel.PubDate       = XmlUtils.GetRfc822DateTime (doc, "/rss/channel/pubDate");
+                channel.Keywords      = XmlUtils.GetXmlNodeText (doc, "/rss/channel/itunes:keywords", mgr);
+                channel.Category      = XmlUtils.GetXmlNodeText (doc, "/rss/channel/itunes:category/@text", mgr);
+
+                channel.LastDownloadTime = DateTime.Now;
+            } catch (Exception e) {
+                Hyena.Log.Exception ("Caught error parsing RSS feed", e);
+                throw;
+            }
+        }
+
+        public IEnumerable<PaasItem> GetItems ()
+        {
+            XmlNodeList nodes = null;
+
+            try {
+                nodes = doc.SelectNodes ("//item");
+            } catch (Exception e) {
+                Hyena.Log.Exception ("Unable to get any RSS items", e);
+            }
+
+            if (nodes != null) {
+                foreach (XmlNode node in nodes) {
+                    PaasItem item = null;
+
+                    try {
+                        item = ParseItem (node);
+                    } catch (Exception e) {
+                        Hyena.Log.Exception (e);
+                    }
+
+                    if (item != null) {
+                        yield return item;
+                    }
+                }
+            }
+        }
+
+        private PaasItem ParseItem (XmlNode node)
+        {
+            try {
+                PaasItem item = new PaasItem ();
+
+                if (!TryParseMediaContent (node, item) && !TryParseEnclosure (node, item)) {
+                    return null;
+                }
+
+                item.Description = StringUtil.RemoveNewlines (XmlUtils.GetXmlNodeText (node, "description", mgr));
+                item.Name = StringUtil.RemoveNewlines (XmlUtils.GetXmlNodeText (node, "title", mgr));
+
+                if (String.IsNullOrEmpty (item.Description) && String.IsNullOrEmpty (item.Name)) {
+                    throw new FormatException ("node:  Either 'title' or 'description' node must exist.");
+                }
+
+                item.Author     = XmlUtils.GetXmlNodeText (node, "author", mgr);
+                item.Comments   = XmlUtils.GetXmlNodeText (node, "comments", mgr);
+
+                item.Link       = XmlUtils.GetXmlNodeText (node, "link", mgr);
+                item.PubDate    = XmlUtils.GetRfc822DateTime (node, "pubDate");
+                item.Modified   = XmlUtils.GetRfc822DateTime (node, "dcterms:modified");
+                item.LicenseUri = XmlUtils.GetXmlNodeText (node, "creativeCommons:license", mgr);
+
+                return item;
+             } catch (Exception e) {
+                 Hyena.Log.Exception ("Caught error parsing RSS item", e);
+             }
+
+             return null;
+        }
+
+        private bool TryParseEnclosure (XmlNode node, PaasItem item)
+        {
+            try {
+                item.Url = XmlUtils.GetXmlNodeText (node, "enclosure/@url", mgr);
+
+                if (String.IsNullOrEmpty (item.Url)) {
+                    return false;
+                }
+
+                item.Size       = Math.Max (0, XmlUtils.GetInt64 (node, "enclosure/@length"));
+                item.MimeType   = XmlUtils.GetXmlNodeText (node, "enclosure/@type", mgr);
+                item.Duration   = GetITunesDuration (node);
+                item.Keywords   = XmlUtils.GetXmlNodeText (node, "itunes:keywords", mgr);
+
+                return true;
+             } catch (Exception e) {
+                 Hyena.Log.Exception ("Caught error parsing RSS enclosure", e);
+             }
+
+             return false;
+        }
+
+        // Parse one Media RSS media:content node
+        // http://search.yahoo.com/mrss/
+        private bool TryParseMediaContent (XmlNode item_node, PaasItem item)
+        {
+            try {
+                XmlNode node = null;
+
+                // Get the highest bitrate "full" content item
+                // TODO allow a user-preference for a feed to decide what quality to get, if there
+                // are options?
+                int max_bitrate = 0;
+                foreach (XmlNode test_node in item_node.SelectNodes ("media:content", mgr)) {
+                    string expr = XmlUtils.GetXmlNodeText (test_node, "@expression", mgr);
+                    if (!(String.IsNullOrEmpty (expr) || expr == "full"))
+                        continue;
+
+                    int bitrate = XmlUtils.GetInt32 (test_node, "@bitrate");
+                    if (node == null || bitrate > max_bitrate) {
+                        node = test_node;
+                        max_bitrate = bitrate;
+                    }
+                }
+
+                if (node == null)
+                    return false;
+
+                item.Url = XmlUtils.GetXmlNodeText (node, "@url", mgr);
+
+                if (item.Url == null) {
+                    return false;
+                }
+
+                item.Size       = Math.Max (0, XmlUtils.GetInt64 (node, "@fileSize"));
+                item.MimeType   = XmlUtils.GetXmlNodeText (node, "@type", mgr);
+                item.Duration   = TimeSpan.FromSeconds (XmlUtils.GetInt64 (node, "@duration", mgr));
+                item.Keywords   = XmlUtils.GetXmlNodeText (node, "itunes:keywords", mgr);
+
+                return true;
+             } catch (Exception e) {
+                 Hyena.Log.Exception ("Caught error parsing RSS media:content", e);
+             }
+
+             return false;
+        }
+
+        private void CheckRss ()
+        {
+            if (doc.SelectSingleNode ("/rss") == null) {
+                throw new FormatException ("Invalid RSS document.");
+            }
+
+            if (XmlUtils.GetXmlNodeText (doc, "/rss/channel/title", mgr) == String.Empty) {
+                throw new FormatException (
+                    "node: 'title', 'description', and 'link' nodes must exist."
+                );
+            }
+
+            mgr = new XmlNamespaceManager (doc.NameTable);
+            mgr.AddNamespace ("itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd";);
+            mgr.AddNamespace ("creativeCommons", "http://backend.userland.com/creativeCommonsRssModule";);
+            mgr.AddNamespace ("media", "http://search.yahoo.com/mrss/";);
+            mgr.AddNamespace ("dcterms", "http://purl.org/dc/terms/";);
+        }
+
+        private TimeSpan GetITunesDuration (XmlNode node)
+        {
+            return GetITunesDuration (XmlUtils.GetXmlNodeText (node, "itunes:duration", mgr));
+        }
+
+        private TimeSpan GetITunesDuration (string duration)
+        {
+            if (String.IsNullOrEmpty (duration)) {
+                return TimeSpan.Zero;
+            }
+
+            int hours = 0, minutes = 0, seconds = 0;
+            string [] parts = duration.Split (':');
+
+            if (parts.Length > 0)
+                seconds = Int32.Parse (parts[parts.Length - 1]);
+
+            if (parts.Length > 1)
+                minutes = Int32.Parse (parts[parts.Length - 2]);
+
+            if (parts.Length > 2)
+                hours = Int32.Parse (parts[parts.Length - 3]);
+
+            return TimeSpan.FromSeconds (hours * 3600 + minutes * 60 + seconds);
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/SyndicationClient.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/SyndicationClient.cs
new file mode 100644
index 0000000..8fa5326
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/SyndicationClient.cs
@@ -0,0 +1,532 @@
+//
+// SyndicationClient.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+using Migo2.Async;
+
+using Banshee.Base;
+using Banshee.ServiceStack;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Aether.Syndication
+{
+    public sealed class SyndicationClient : AetherClient
+    {
+        private bool disposed;
+
+        private PaasSource source;
+        private ChannelUpdateManager channel_manager;
+
+        private Dictionary<long, DeletedChannelInfo> deleted;
+        private Dictionary<long, ChannelUpdateTask> updating;
+
+        public event EventHandler<ItemEventArgs>    ItemsAdded;
+        public event EventHandler<ItemEventArgs>    ItemsRemoved;
+
+        public event EventHandler<ChannelEventArgs> ChannelsAdded;
+        public event EventHandler<ChannelEventArgs> ChannelsRemoved;
+
+        public event EventHandler<ChannelEventArgs> ChannelUpdating;
+        public event EventHandler<ChannelUpdateCompletedEventArgs> ChannelUpdateCompleted;
+
+        public SyndicationClient (PaasSource source)
+        {
+            this.source = source;
+            channel_manager = new ChannelUpdateManager (2);
+
+            channel_manager.Started += (sender, e) => {
+                OnStateChanged (AetherClientState.Idle, AetherClientState.Busy);
+            };
+
+            channel_manager.Stopped += (sender, e) => {
+                OnStateChanged (AetherClientState.Busy, AetherClientState.Idle);
+            };
+
+            channel_manager.TaskStarted += (sender, e) => {
+                OnChannelUpdating (e.Task.UserState as PaasChannel);
+            };
+
+            channel_manager.TaskAdded += (sender, e) => {
+                if (e.Task != null) {
+                    OnChannelUpdating (e.Task.UserState as PaasChannel);
+                } else {
+                    foreach (Task t in e.Tasks) {
+                        OnChannelUpdating (t.UserState as PaasChannel);
+                    }
+                }
+            };
+
+            channel_manager.TaskCompleted += TaskCompletedHandler;
+
+            deleted = new Dictionary<long, DeletedChannelInfo> ();
+            updating = new Dictionary<long, ChannelUpdateTask> ();
+        }
+
+        public override void Dispose ()
+        {
+            lock (SyncRoot) {
+                disposed = true;
+            }
+
+            channel_manager.Dispose ();
+            channel_manager.TaskCompleted -= TaskCompletedHandler;
+
+            source = null;
+            channel_manager = null;
+
+            base.Dispose ();
+        }
+
+        public ChannelUpdateStatus GetUpdateStatus (PaasChannel channel)
+        {
+            lock (SyncRoot) {
+                ChannelUpdateTask task;
+
+                if (channel != null && updating.TryGetValue (channel.DbId, out task)) {
+                    return (task.State == TaskState.Running) ?
+                        ChannelUpdateStatus.Updating : ChannelUpdateStatus.Waiting;
+                }
+
+                return ChannelUpdateStatus.None;
+            }
+        }
+
+
+        public void DeleteChannel (PaasChannel channel)
+        {
+            DeleteChannel (channel, false);
+        }
+
+        public void DeleteChannel (PaasChannel channel, bool deleteFiles)
+        {
+            if (channel == null) {
+                throw new ArgumentNullException ("channel");
+            }
+
+            lock (SyncRoot) {
+                if (!disposed) {
+                    if (updating.ContainsKey (channel.DbId)) {
+                        deleted.Add (
+                            channel.DbId,
+                            new DeletedChannelInfo { DeleteFiles = deleteFiles, Channel = channel }
+                        );
+
+                        updating[channel.DbId].CancelAsync ();
+                    } else {
+                        DeleteChannelImpl (channel, deleteFiles);
+                    }
+                }
+            }
+        }
+
+        public void DeleteChannels (IEnumerable<PaasChannel> channels, bool deleteFiles)
+        {
+            if (channels == null) {
+                throw new ArgumentNullException ("channels");
+            }
+
+            foreach (PaasChannel channel in channels) {
+                DeleteChannel (channel, deleteFiles);
+            }
+        }
+
+        private void DeleteChannelImpl (PaasChannel channel, bool deleteFiles)
+        {
+            List<PaasItem> items = new List<PaasItem> (channel.Items);
+
+            ServiceManager.DbConnection.BeginTransaction ();
+
+            try {
+                if (items != null) {
+                    DeleteItems (items, deleteFiles, true);
+                }
+
+                PaasChannel.Provider.Delete (channel);
+                ServiceManager.DbConnection.CommitTransaction ();
+            } catch {
+                ServiceManager.DbConnection.RollbackTransaction ();
+                throw;
+            }
+
+            OnChannelRemoved (channel);
+        }
+
+        private void DeleteItems (IEnumerable<PaasItem> items, bool deleteFiles, bool notify)
+        {
+            if (items == null) {
+                throw new ArgumentNullException ("items");
+            }
+
+            lock (SyncRoot) {
+                if (!disposed) {
+                    source.RemoveItems (items);
+                    PaasItem.Provider.Delete (items);
+
+                    foreach (var item in items) {
+                        if (deleteFiles && item.IsDownloaded) {
+                            try  {
+                                Banshee.IO.Utilities.DeleteFileTrimmingParentDirectories (new SafeUri (item.LocalPath));
+                            } catch {}
+                        }
+                    }
+
+                    if (notify) {
+                        OnItemsRemoved (items);
+                    }
+                }
+            }
+        }
+
+        public void RemoveItem (PaasItem item)
+        {
+            RemoveItem (item, false);
+        }
+
+        public void RemoveItem (PaasItem item, bool keepFile)
+        {
+            if (item == null) {
+                throw new ArgumentNullException ("item");
+            }
+
+            lock (SyncRoot) {
+                if (!disposed) {
+                    item.Active = false;
+                    item.Save ();
+
+                    OnItemRemoved (item);
+                }
+            }
+        }
+
+        public void RemoveItems (IEnumerable<PaasItem> items)
+        {
+            RemoveItems (items, true);
+        }
+
+        public void RemoveItems (IEnumerable<PaasItem> items, bool deleteFiles)
+        {
+            if (items == null) {
+                throw new ArgumentNullException ("items");
+            }
+
+            lock (SyncRoot) {
+                if (!disposed) {
+                    ServiceManager.DbConnection.BeginTransaction ();
+
+                    try {
+                        foreach (PaasItem item in items) {
+                            item.Active = false;
+                            item.Save ();
+
+                            if (deleteFiles && item.IsDownloaded) {
+                                try  {
+                                    Banshee.IO.Utilities.DeleteFileTrimmingParentDirectories (new SafeUri (item.LocalPath));
+                                } catch {}
+                            }
+                        }
+
+                        ServiceManager.DbConnection.CommitTransaction ();
+                        OnItemsRemoved (items);
+                    } catch (Exception e) {
+                        Hyena.Log.Exception (e);
+                        ServiceManager.DbConnection.RollbackTransaction ();
+                    }
+                }
+            }
+        }
+
+        public void SubscribeToChannel (string url, DownloadPreference download_pref)
+        {
+            if (!IsValidUrl (url)) {
+                throw new ArgumentException ("Invalid URL!", "url");
+            }
+
+            lock (SyncRoot) {
+                if (disposed) {
+                    return;
+                }
+
+                PaasChannel channel = PaasChannel.Provider.FetchFirstMatching ("Url = ?", url);
+
+                if (channel == null)  {
+                    channel = new PaasChannel () {
+                        Url = url,
+                        DownloadPreference = download_pref,
+                        ClientID = (long) AetherClientID.Syndication
+                    };
+
+                    channel.Save ();
+                    OnChannelAdded (channel);
+                }
+            }
+        }
+
+        public void UpdateAsync ()
+        {
+            lock (SyncRoot) {
+                if (disposed) {
+                    return;
+                }
+
+                QueueUpdate (
+                    PaasChannel.Provider.FetchAllMatching (
+                        "ClientID = ? ORDER BY HYENA_COLLATION_KEY(Name), Url", (long) AetherClientID.Syndication
+                    )
+                );
+            }
+        }
+
+        public void QueueUpdate (PaasChannel channel)
+        {
+            lock (SyncRoot) {
+                if (disposed) {
+                    return;
+                }
+
+                if (!updating.ContainsKey (channel.DbId)) {
+                    ChannelUpdateTask task = new ChannelUpdateTask (channel);
+                    updating[channel.DbId] = task;
+                    channel_manager.Add (task);
+                }
+            }
+        }
+
+        public void QueueUpdate (IEnumerable<PaasChannel> channels)
+        {
+            lock (SyncRoot) {
+                if (disposed) {
+                    return;
+                }
+
+                List<ChannelUpdateTask> tasks = new List<ChannelUpdateTask> ();
+
+                foreach (PaasChannel channel in channels.Where (
+                    (channel) =>
+                        channel.ClientID == (long) AetherClientID.Syndication &&
+                        !updating.ContainsKey (channel.DbId)
+                    )) {
+
+                    ChannelUpdateTask task = new ChannelUpdateTask (channel);
+                    updating[channel.DbId] = task;
+                    tasks.Add (task);
+                }
+
+                channel_manager.Add (tasks);
+            }
+        }
+
+        private bool IsValidUrl (string url)
+        {
+            try {
+                Uri uri = new Uri (url);
+
+                if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) {
+                    return true;
+                }
+            } catch {}
+
+            return false;
+        }
+
+        private void TaskCompletedHandler (object sender, TaskCompletedEventArgs<ChannelUpdateTask> e)
+        {
+            // TODO - Check encoding
+            ChannelUpdateTask task = e.Task;
+            PaasChannel channel = task.Channel;
+
+            List<PaasItem> new_items = null;
+            List<PaasItem> removed_items = null;
+
+            lock (SyncRoot) {
+                if (disposed) {
+                    return;
+                }
+
+                if (deleted.ContainsKey (channel.DbId)) {
+                    DeleteChannelImpl (channel, deleted[channel.DbId].DeleteFiles);
+
+                    deleted.Remove (channel.DbId);
+                    updating.Remove (channel.DbId);
+                    OnChannelUpdateCompleted (channel, false, null);
+                    return;
+                }
+
+                try {
+                    if (e.Error == null && e.State == TaskState.Succeeded) {
+                        RssParser parser = new RssParser (task.Result);
+
+                        ServiceManager.DbConnection.BeginTransaction ();
+
+                        try {
+                            parser.UpdateChannel (channel);
+                            channel.Save ();
+
+                            var cmp = new PaasItemEqualityComparer ();
+
+                            IEnumerable<PaasItem> local_items = channel.Items;
+                            IEnumerable<PaasItem> remote_items = parser.GetItems ().Distinct (cmp);
+
+                            new_items = new List<PaasItem> (remote_items.Except (local_items, cmp));
+                            removed_items = new List<PaasItem> (
+                                local_items.Except (remote_items, cmp).Where ((i) => (!i.IsDownloaded || !i.Active))
+                            );
+
+                            if (new_items.Count > 0) {
+                                foreach (PaasItem item in new_items) {
+                                    item.IsNew = true;
+                                    item.Channel = channel;
+                                    item.ClientID = (int)AetherClientID.Syndication;
+                                    item.Save ();
+                                }
+
+                                source.AddItems (new_items);
+                            }
+
+                            if (removed_items.Count > 0) {
+                                DeleteItems (removed_items, false, false);
+                            }
+                        } catch {
+                            ServiceManager.DbConnection.RollbackTransaction ();
+                            throw;
+                        }
+
+                        ServiceManager.DbConnection.CommitTransaction ();
+
+                        if (new_items != null && new_items.Count > 0) {
+                            OnItemsAdded (new_items);
+                        }
+
+                        if (removed_items != null && removed_items.Count > 0) {
+                            OnItemsRemoved (removed_items);
+                        }
+                    }
+
+                    OnChannelUpdateCompleted (channel, e.Error);
+                } catch (Exception ex) {
+                    Hyena.Log.Exception (ex);
+                    OnChannelUpdateCompleted (channel, ex);
+                } finally {
+                    updating.Remove (channel.DbId);
+                }
+            }
+        }
+
+        private void OnChannelAdded (PaasChannel channel)
+        {
+            var handler = ChannelsAdded;
+
+            if (handler != null) {
+                EventQueue.Register (
+                    new EventWrapper<ChannelEventArgs> (handler, this, new ChannelEventArgs (channel))
+                );
+            }
+        }
+
+        private void OnChannelRemoved (PaasChannel channel)
+        {
+            var handler = ChannelsRemoved;
+
+            if (handler != null) {
+                EventQueue.Register (
+                    new EventWrapper<ChannelEventArgs> (handler, this, new ChannelEventArgs (channel))
+                );
+            }
+        }
+
+        private void OnChannelUpdating (PaasChannel channel)
+        {
+            var handler = ChannelUpdating;
+
+            if (handler != null) {
+                EventQueue.Register (
+                    new EventWrapper<ChannelEventArgs> (handler, this, new ChannelEventArgs (channel))
+                );
+            }
+        }
+
+        private void OnChannelUpdateCompleted (PaasChannel channel, Exception err)
+        {
+            OnChannelUpdateCompleted (channel, err == null, err);
+        }
+
+        private void OnChannelUpdateCompleted (PaasChannel channel, bool succeeded, Exception e)
+        {
+            var handler = ChannelUpdateCompleted;
+
+            if (handler != null) {
+                EventQueue.Register (
+                    new EventWrapper<ChannelUpdateCompletedEventArgs> (
+                        handler, this, new ChannelUpdateCompletedEventArgs (channel, succeeded, e)
+                    )
+                );
+            }
+        }
+
+        private void OnItemsAdded (IEnumerable<PaasItem> items)
+        {
+            var handler = ItemsAdded;
+
+            if (handler != null) {
+                EventQueue.Register (
+                    new EventWrapper<ItemEventArgs> (handler, this, new ItemEventArgs (items))
+                );
+            }
+        }
+
+        private void OnItemRemoved (PaasItem item)
+        {
+            var handler = ItemsRemoved;
+
+            if (handler != null) {
+                EventQueue.Register (
+                    new EventWrapper<ItemEventArgs> (handler, this, new ItemEventArgs (item))
+                );
+            }
+        }
+
+        private void OnItemsRemoved (IEnumerable<PaasItem> items)
+        {
+            var handler = ItemsRemoved;
+
+            if (handler != null) {
+                EventQueue.Register (
+                    new EventWrapper<ItemEventArgs> (handler, this, new ItemEventArgs (items))
+                );
+            }
+        }
+
+
+        private class DeletedChannelInfo
+        {
+            public bool DeleteFiles { get; set; }
+            public PaasChannel Channel { get; set; }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/CacheModelProvider.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/CacheModelProvider.cs
new file mode 100644
index 0000000..802a4ff
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/CacheModelProvider.cs
@@ -0,0 +1,142 @@
+//
+// CacheModelProvider.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Threading;
+using System.Collections.Generic;
+
+using Hyena;
+using Hyena.Data;
+using Hyena.Data.Sqlite;
+
+namespace Banshee.Paas.Data
+{
+    // Caches all results retrieved from the database, such that any subsequent retrieval will
+    // return the same instance.
+    public class CacheModelProvider<T> : SqliteModelProvider<T> where T : CacheableItem<T>, new()
+    {
+        private ReaderWriterLock rw_lock = new ReaderWriterLock ();
+        private Dictionary<long, T> full_cache = new Dictionary<long, T> ();
+
+        public CacheModelProvider (HyenaSqliteConnection connection, string table_name) : base (connection, table_name)
+        {
+        }
+
+        #region Overrides
+
+        public override T FetchSingle (long id)
+        {
+            return GetCached (id) ?? CacheResult (base.FetchSingle (id));
+        }
+
+        public override void Save (T target)
+        {
+            base.Save (target);
+            rw_lock.AcquireWriterLock (-1);
+
+            try {
+                if (!full_cache.ContainsKey (target.DbId)) {
+                    full_cache[target.DbId] = target;
+                }
+            } finally {
+                rw_lock.ReleaseWriterLock ();
+            }
+        }
+
+        public override T Load (System.Data.IDataReader reader)
+        {
+            return GetCached (PrimaryKeyFor (reader)) ?? CacheResult (base.Load (reader));
+        }
+
+        public override void Delete (long id)
+        {
+            base.Delete (id);
+            rw_lock.AcquireWriterLock (-1);
+
+            try {
+                full_cache.Remove (id);
+            } finally {
+                rw_lock.ReleaseWriterLock ();
+            }
+        }
+
+        public override void Delete (IEnumerable<T> items)
+        {
+            base.Delete (items);
+            rw_lock.AcquireWriterLock (-1);
+
+            try {
+                foreach (T item in items) {
+                    if (item != null) {
+                        full_cache.Remove (PrimaryKeyFor (item));
+                    }
+                }
+            } finally {
+                rw_lock.ReleaseWriterLock ();
+            }
+        }
+
+        #endregion
+
+#region Utility Methods
+
+        private T GetCached (long id)
+        {
+            rw_lock.AcquireReaderLock (-1);
+
+            try {
+                if (full_cache.ContainsKey (id)) {
+                    return full_cache[id];
+                } else {
+                    return null;
+                }
+            } finally {
+                rw_lock.ReleaseReaderLock ();
+            }
+        }
+
+        private T CacheResult (T item)
+        {
+            if (item == null) {
+                return null;
+            }
+
+            rw_lock.AcquireWriterLock (-1);
+
+            try {
+                full_cache[item.DbId] = item;
+                return item;
+            } finally {
+                rw_lock.ReleaseWriterLock ();
+            }
+        }
+
+#endregion
+
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/CacheableItem.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/CacheableItem.cs
new file mode 100644
index 0000000..0005b4f
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/CacheableItem.cs
@@ -0,0 +1,54 @@
+//
+// CacheableItem.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+using Hyena;
+using Hyena.Data;
+using Hyena.Data.Sqlite;
+
+namespace Banshee.Paas.Data
+{
+    // This class is generic b/c of some ideas that I didn't implement yet...could be made non-generic
+    public abstract class CacheableItem<T> : ICacheableItem
+    {
+        private object cache_entry_id;
+        public object CacheEntryId {
+            get { return cache_entry_id; }
+            set { cache_entry_id = value; }
+        }
+
+        private long cache_model_id;
+        public long CacheModelId {
+            get { return cache_model_id; }
+            set { cache_model_id = value; }
+        }
+
+        public abstract long DbId { get; protected set;}
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/DownloadPreference.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/DownloadPreference.cs
new file mode 100644
index 0000000..af157d1
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/DownloadPreference.cs
@@ -0,0 +1,35 @@
+//
+// DownloadPreference.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+namespace Banshee.Paas.Data
+{
+    public enum DownloadPreference : int
+    {
+        All  = 0,
+        One  = 1,
+        None = 2
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/DownloadStatusFilterModel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/DownloadStatusFilterModel.cs
new file mode 100644
index 0000000..17a6682
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/DownloadStatusFilterModel.cs
@@ -0,0 +1,86 @@
+//
+// DownloadStatusFilterModel.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using Banshee.Collection;
+using Banshee.Collection.Database;
+
+namespace Banshee.Paas.Gui
+{
+    public enum DownloadedStatusFilter
+    {
+        Both,
+        Downloaded,
+        NotDownloaded
+    }
+
+    public class DownloadStatusFilterModel : FilterListModel<DownloadedStatusFilter>
+    {
+        public DownloadStatusFilterModel (DatabaseTrackListModel trackModel) : base (trackModel)
+        {
+            Selection.Clear (false);
+            Selection.QuietSelect (0);
+        }
+
+        public override void Reload (bool notify)
+        {
+            if (notify)
+                OnReloaded ();
+        }
+
+        public override void Clear ()
+        {
+        }
+
+        public override DownloadedStatusFilter this [int index] {
+            get {
+                switch (index) {
+                    case 1:    return DownloadedStatusFilter.Downloaded;
+                    case 2:    return DownloadedStatusFilter.NotDownloaded;
+                    case 0:
+                    default:   return DownloadedStatusFilter.Both;
+                }
+            }
+        }
+
+        public override int Count {
+            get { return 3; }
+        }
+
+        public override string GetSqlFilter ()
+        {
+            if (Selection.AllSelected) {
+                return null;
+            } else if (Selection.Contains (1)) {
+                return "PaasItems.LocalPath NOT NULL";
+            } else {
+                return "PaasItems.LocalPath IS NULL";
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/ListModel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/ListModel.cs
new file mode 100644
index 0000000..bc25f01
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/ListModel.cs
@@ -0,0 +1,201 @@
+//
+// ListModel.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+using Migo2.Collections;
+
+using Hyena.Data;
+using Hyena.Collections;
+
+using Banshee.Base;
+using Banshee.Paas.Utils;
+
+namespace Banshee.Paas.Data
+{
+    public class ListModel<T> : IListModel<T>
+    {
+        public event EventHandler Cleared;
+        public event EventHandler Reloaded;
+
+        private List<T> items;
+        private Selection selection;
+
+        public virtual bool CanReorder {
+            get { return false; }
+        }
+
+        public virtual int Count {
+            get { return items.Count; }
+        }
+
+        public Selection Selection {
+            get { return selection; }
+        }
+
+        public T this[int index] {
+            get { return GetIndex (index); }
+        }
+
+        protected List<T> Items {
+            get { return items; }
+        }
+
+        public ListModel () : this (null)
+        {
+        }
+
+        public ListModel (Selection selection)
+        {
+            items = new List<T> ();
+            this.selection = selection ?? new Selection ();
+        }
+
+        public void Add (T item)
+        {
+            if (!item.Equals (default (T))) {
+                items.Add (item);
+                OnReloaded ();
+            }
+        }
+
+        public void Add (IEnumerable<T> items)
+        {
+            if (items != null) {
+                foreach (T item in items) {
+                    if (item != null) {
+                        this.items.Add (item);
+                    }
+                }
+
+                OnReloaded ();
+            }
+        }
+
+        public IEnumerable<T> GetSelected ()
+        {
+            T item = default (T);
+            List<T> selected = new List<T> ();
+
+            foreach (int i in selection) {
+                item = GetIndex (i);
+
+                if (item != null) {
+                    selected.Add (item);
+                }
+            }
+
+            return selected;
+        }
+
+        public void Remove (T item)
+        {
+            if (item != null) {
+                items.Remove (item);
+                OnReloaded ();
+            }
+        }
+
+        public void Remove (IEnumerable<T> items)
+        {
+            if (items != null) {
+                foreach (T item in items) {
+                    if (item != null) {
+                        this.items.Remove (item);
+                    }
+                }
+
+                OnReloaded ();
+            }
+        }
+
+        public void Clear ()
+        {
+            items.Clear ();
+            OnCleared ();
+        }
+
+        public void Reload ()
+        {
+            OnReloaded ();
+        }
+
+        public void Reorder (int[] newWorldOrder)
+        {
+            int len = newWorldOrder.Length;
+            int[] order = new int[len];
+            Dictionary<T, int> positions = new Dictionary<T, int> (len);
+
+            int i = 0;
+            for (; i < order.Length; ++i) {
+                order[newWorldOrder[i]] = i;
+            }
+
+            i = 0;
+            foreach (var t in items) {
+                positions.Add (t, order[i++]);
+            }
+
+            items.Sort (new OrderComparer<T> (positions));
+            selection.Clear ();
+
+            OnReloaded ();
+        }
+
+        private T GetIndex (int index)
+        {
+            if (index >= 0 && index < items.Count) {
+                return items[index];
+            }
+
+            return default (T);
+        }
+
+        protected virtual void OnReloaded ()
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                EventHandler handler = Reloaded;
+
+                if (handler != null) {
+                    handler (this, EventArgs.Empty);
+                }
+            });
+        }
+
+        protected virtual void OnCleared ()
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                EventHandler handler = Cleared;
+
+                if (handler != null) {
+                    handler (this, EventArgs.Empty);
+                }
+            });
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasChannel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasChannel.cs
new file mode 100644
index 0000000..5525340
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasChannel.cs
@@ -0,0 +1,223 @@
+//
+// PaasChannel.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Banshee.ServiceStack;
+
+using Hyena;
+using Hyena.Data;
+using Hyena.Data.Sqlite;
+
+using Banshee.Paas.Utils;
+
+namespace Banshee.Paas.Data
+{
+    public class PaasChannelProvider : CacheModelProvider<PaasChannel>
+    {
+        public PaasChannelProvider (HyenaSqliteConnection connection) : base (connection, "PaasChannels")
+        {
+        }
+    }
+
+    public class PaasChannel : CacheableItem<PaasChannel>
+    {
+        private static PaasChannel empty;
+        private static PaasChannelProvider provider;
+
+        static PaasChannel () {
+            empty = new PaasChannel ();
+            provider = new PaasChannelProvider (ServiceManager.DbConnection);
+        }
+
+        public static PaasChannel Empty {
+            get { return empty; }
+        }
+        public static PaasChannelProvider Provider {
+            get { return provider; }
+        }
+
+        private long dbid;
+        [DatabaseColumn ("ID", Constraints = DatabaseColumnConstraints.PrimaryKey)]
+        public override long DbId {
+            get { return dbid; }
+            protected set { dbid = value; }
+        }
+
+        private long client_id;
+        [DatabaseColumn ("ClientID", Index = "PaasChannelClientIDIndex")]
+        public long ClientID {
+            get { return client_id; }
+            set { client_id = value; }
+        }
+
+        private long external_id;
+        [DatabaseColumn ("ExternalID", Index = "PaasChannelExternalIDIndex")]
+        public long ExternalID {
+            get { return external_id; }
+            set { external_id = value; }
+        }
+
+        private string category;
+        [DatabaseColumn]
+        public string Category {
+            get { return category; }
+            set { category = value; }
+        }
+
+        private string copyright;
+        [DatabaseColumn]
+        public string Copyright {
+            get { return copyright; }
+            set { copyright = value; }
+        }
+
+        private string description;
+        [DatabaseColumn]
+        public string Description {
+            get { return description; }
+            set {
+                description = value;
+                StrippedDescription = StringUtils.StripHtml (value);
+            }
+        }
+
+        private DownloadPreference download_preference;
+        [DatabaseColumn]
+        public DownloadPreference DownloadPreference {
+            get { return download_preference; }
+            set { download_preference = value; }
+        }
+
+        private string image_url;
+        [DatabaseColumn]
+        public string ImageUrl {
+            get { return image_url; }
+            set { image_url = value; }
+        }
+
+        private string language;
+        [DatabaseColumn]
+        public string Language {
+            get { return language; }
+            set { language = value; }
+        }
+
+        private DateTime last_build_date;
+        [DatabaseColumn]
+        public DateTime LastBuildDate {
+            get { return last_build_date; }
+            set { last_build_date = value; }
+        }
+
+        private DateTime last_download_time;
+        [DatabaseColumn]
+        public DateTime LastDownloadTime {
+            get { return last_download_time; }
+            set { last_download_time = value; }
+        }
+
+        private string license;
+        [DatabaseColumn]
+        public string License {
+            get { return license; }
+            set { license = value; }
+        }
+
+        private string link;
+        [DatabaseColumn]
+        public string Link {
+            get { return link; }
+            set { link = value; }
+        }
+
+        private string local_enclosure_path;
+        [DatabaseColumn]
+        public string LocalEnclosurePath {
+            get { return local_enclosure_path; }
+            set { local_enclosure_path = value; }
+        }
+
+        private string name;
+        [DatabaseColumn (Index = "PaasChannelNameIndex")]
+        public string Name {
+            get {
+                return String.IsNullOrEmpty (name) ? url : name;
+            }
+
+            set { name = value; }
+        }
+
+        private DateTime pub_date;
+        [DatabaseColumn]
+        public DateTime PubDate {
+            get { return pub_date; }
+            set { pub_date = value; }
+        }
+
+        private string publisher;
+        [DatabaseColumn]
+        public string Publisher {
+            get { return publisher; }
+            set { publisher = value; }
+        }
+
+        private string stripped_description;
+        [DatabaseColumn]
+        public string StrippedDescription {
+            get { return stripped_description; }
+            set { stripped_description = value; }
+        }
+
+        private string keywords;
+        [DatabaseColumn]
+        public string Keywords {
+            get { return keywords; }
+            set { keywords = value; }
+        }
+
+        private string url;
+        [DatabaseColumn]
+        public string Url {
+            get { return url; }
+            set { url = value; }
+        }
+
+        public IEnumerable<PaasItem> Items {
+            get {
+                foreach (PaasItem item in PaasItem.Provider.FetchAllMatching ("ChannelID = ?", DbId)) {
+                    yield return item;
+                }
+            }
+        }
+
+        public void Save ()
+        {
+            Provider.Save (this);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasChannelModel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasChannelModel.cs
new file mode 100644
index 0000000..4ddc6ec
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasChannelModel.cs
@@ -0,0 +1,62 @@
+//
+// PaasChannelModel.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Mono.Unix;
+
+using Hyena.Data;
+
+using Banshee.Database;
+using Banshee.Collection.Database;
+
+namespace Banshee.Paas.Data
+{
+    public class PaasChannelModel : DatabaseFilterListModel<PaasChannel, PaasChannel>
+    {
+        public PaasChannelModel (Banshee.Sources.DatabaseSource source, DatabaseTrackListModel trackModel, BansheeDbConnection connection, string uuid)
+            : base ("paas", Catalog.GetString ("Paas"), source, trackModel, connection, PaasChannel.Provider, new PaasChannel (), uuid)
+        {
+            ReloadFragmentFormat = "FROM PaasChannels ORDER BY HYENA_COLLATION_KEY(Name)";
+        }
+
+        public override string FilterColumn {
+            get { return PaasChannel.Provider.PrimaryKey; }
+        }
+
+        protected override string ItemToFilterValue (object item)
+        {
+            return (item != select_all_item && item is PaasChannel) ? (item as PaasChannel).DbId.ToString () : null;
+        }
+
+        public override void UpdateSelectAllItem (long count)
+        {
+            select_all_item.Name = String.Format (Catalog.GetString ("All Channels ({0})"), count);
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasItem.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasItem.cs
new file mode 100644
index 0000000..e25a03f
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasItem.cs
@@ -0,0 +1,323 @@
+//
+// PaasItem.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Data;
+using System.Collections.Generic;
+
+using Hyena;
+using Hyena.Data;
+using Hyena.Data.Sqlite;
+
+using Banshee.ServiceStack;
+using Banshee.Collection.Database;
+
+using Banshee.Paas.Utils;
+using Banshee.Paas.DownloadManager.Data;
+
+namespace Banshee.Paas.Data
+{
+    public class PaasItemProvider : CacheModelProvider<PaasItem>
+    {
+        public PaasItemProvider (HyenaSqliteConnection connection) : base (connection, "PaasItems")
+        {
+        }
+
+        public IEnumerable<PaasItem> FetchQueued (long primarySourceID)
+        {
+            string restore_command = String.Format (
+                @"SELECT {0} FROM {1}
+                    JOIN {2} ON {1}.ID = {2}.ExternalID
+                  WHERE PrimarySourceID = ? AND {1}.LocalPath IS NULL
+                    ORDER BY {2}.Position ASC", Select, From,
+                    QueuedDownloadTask.Provider.From
+            );
+
+            using (IDataReader reader = Connection.Query (restore_command, primarySourceID)) {
+                while (reader.Read ()) {
+                    yield return Load (reader);
+                }
+            }
+        }
+    }
+
+    public class PaasItem : CacheableItem<PaasItem>
+    {
+        private long dbid;
+
+        private static PaasItem empty;
+        private static PaasItemProvider provider;
+
+        static PaasItem () {
+            empty = new PaasItem ();
+            provider = new PaasItemProvider (ServiceManager.DbConnection);
+        }
+
+        public static PaasItem Empty {
+            get { return empty; }
+        }
+
+        public static PaasItemProvider Provider {
+            get { return provider; }
+        }
+
+        [DatabaseColumn ("ID", Constraints = DatabaseColumnConstraints.PrimaryKey)]
+        public override long DbId {
+            get { return dbid; }
+            protected set { dbid = value; }
+        }
+
+        private long client_id;
+        [DatabaseColumn ("ClientID", Index = "PaasItemsClientIDIndex")]
+        public long ClientID {
+            get { return client_id; }
+            set { client_id = value; }
+        }
+
+        private long external_id;
+        [DatabaseColumn ("ExternalID", Index = "PaasItemsExternalIDIndex")]
+        public long ExternalID {
+            get { return external_id; }
+            set { external_id = value; }
+        }
+
+        private long external_channel_id;
+        [DatabaseColumn ("ExternalChannelID", Index = "PaasItemsExternalChannelIDIndex")]
+        public long ExternalChannelID {
+            get { return external_channel_id; }
+            set { external_channel_id = value; }
+        }
+
+        private long channel_id;
+        [DatabaseColumn ("ChannelID", Index = "PaasItemChannelIDIndex")]
+        public long ChannelID {
+            get { return channel_id; }
+            set {
+                channel = null;
+                channel_id = value;
+            }
+        }
+
+        private bool active = true;
+        [DatabaseColumn (Index = "PaasItemActiveIndex")]
+        public bool Active {
+            get { return active; }
+            set { active = value; }
+        }
+
+        private DateTime date;
+        [DatabaseColumn (Index = "PaasItemPubDateIndex")]
+        public DateTime PubDate {
+            get { return date; }
+            set { date = value; }
+        }
+
+        private string author;
+        [DatabaseColumn]
+        public string Author {
+            get { return author; }
+            set { author = value; }
+        }
+
+        private string comments;
+        [DatabaseColumn]
+        public string Comments {
+            get { return comments; }
+            set { comments = value; }
+        }
+
+        private string description;
+        [DatabaseColumn]
+        public string Description {
+            get { return description; }
+            set {
+                description = value;
+                StrippedDescription = StringUtils.StripHtml (value);
+            }
+        }
+
+        private DateTime downloaded_at;
+        [DatabaseColumn (Index = "PaasItemDownloadedAtIndex")]
+        public DateTime DownloadedAt {
+            get { return downloaded_at; }
+            internal set { downloaded_at = value; }
+        }
+
+        private TimeSpan duration;
+        [DatabaseColumn]
+        public TimeSpan Duration {
+            get { return duration; }
+            set { duration = value; }
+        }
+
+        private bool error;
+        [DatabaseColumn]
+        public bool Error {
+            get { return error; }
+            set { error = value; }
+        }
+
+        public bool IsDownloaded {
+            get {
+                return !String.IsNullOrEmpty (LocalPath);
+            }
+        }
+
+        private bool is_new;
+        [DatabaseColumn (Index = "PaasItemIsNewIndex")]
+        public bool IsNew {
+            get { return is_new; }
+            set { is_new = value; }
+        }
+
+        private string image;
+        [DatabaseColumn]
+        public string ImageUrl {
+            get { return image; }
+            set { image = value; }
+        }
+
+        private string keywords;
+        [DatabaseColumn]
+        public string Keywords {
+            get { return keywords; }
+            set { keywords = value; }
+        }
+
+        private string license_uri;
+        [DatabaseColumn]
+        public string LicenseUri {
+            get { return license_uri; }
+            set { license_uri = value; }
+        }
+
+        private string link;
+        [DatabaseColumn]
+        public string Link {
+            get { return link; }
+            set { link = value; }
+        }
+
+        private string local_path;
+        [DatabaseColumn (Index = "PaasItemLocalPathIndex")]
+        public string LocalPath {
+            get { return local_path; }
+            set { local_path = value; }
+        }
+
+        private string mime_type;
+        [DatabaseColumn]
+        public string MimeType {
+            get { return mime_type; }
+            set {
+                mime_type = value;
+
+                if (String.IsNullOrEmpty (mime_type)) {
+                    mime_type = "application/octet-stream";
+                }
+            }
+        }
+
+        private DateTime modified;
+        [DatabaseColumn]
+        public DateTime Modified {
+            get { return modified; }
+            set { modified = value; }
+        }
+
+        private string name;
+        [DatabaseColumn (Index = "PaasItemNameIndex")]
+        public string Name {
+            get { return name; }
+            set { name = value; }
+        }
+
+        private long size;
+        [DatabaseColumn]
+        public long Size {
+            get { return size; }
+            set { size = value; }
+        }
+
+        private string stripped_description;
+        public string StrippedDescription {
+            get { return stripped_description; }
+            protected set {
+                stripped_description = value;
+            }
+        }
+
+        private string url;
+        [DatabaseColumn]
+        public string Url {
+            get { return url; }
+            set { url = value; }
+        }
+
+        private PaasChannel channel;
+        public PaasChannel Channel {
+            get {
+                if (channel == null && ChannelID > 0) {
+                    channel = PaasChannel.Provider.FetchSingle (ChannelID);
+                }
+
+                return channel ?? PaasChannel.Empty;
+            }
+
+            set {
+                if (value != null) {
+                    channel = value;
+                    channel_id = channel.DbId;
+                }
+            }
+        }
+
+        public void Save ()
+        {
+            if (this != empty) {
+                Provider.Save (this);
+            }
+        }
+
+        public void Delete (bool delete_file)
+        {
+            Provider.Delete (this);
+        }
+    }
+
+    public class PaasItemEqualityComparer : IEqualityComparer<PaasItem>
+    {
+        public bool Equals (PaasItem lhs, PaasItem rhs)
+        {
+            return (lhs.Url == rhs.Url || lhs.Name == rhs.Name) && lhs.PubDate == rhs.PubDate;
+        }
+
+        public int GetHashCode (PaasItem obj)
+        {
+            return base.GetHashCode ();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasTrackInfo.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasTrackInfo.cs
new file mode 100644
index 0000000..b5f7757
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasTrackInfo.cs
@@ -0,0 +1,189 @@
+//
+// PaasTrackInfo.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Banshee.Collection;
+using Banshee.Collection.Database;
+
+namespace Banshee.Paas.Data
+{
+    public class PaasTrackInfo
+    {
+        private static readonly object sync = new object ();
+
+        public static PaasTrackInfo From (TrackInfo track)
+        {
+            if (track != null) {
+                PaasTrackInfo pi = track.ExternalObject as PaasTrackInfo;
+                PaasItem item = (pi != null) ? pi.Item : null;
+
+                if (pi != null && item != null) {
+                    track.ReleaseDate = item.PubDate;
+                }
+
+                return pi;
+            }
+
+            return null;
+        }
+
+        public static IEnumerable<PaasTrackInfo> From (IEnumerable<TrackInfo> tracks)
+        {
+            lock (sync) {
+                foreach (TrackInfo track in tracks) {
+                    PaasTrackInfo pi = From (track);
+
+                    if (pi != null) {
+                        yield return pi;
+                    }
+                }
+            }
+        }
+
+        private int position;
+        private DatabaseTrackInfo track;
+
+        private PaasItem item;
+
+        public DatabaseTrackInfo Track {
+            get { return track; }
+        }
+
+        public PaasChannel Channel {
+            get { return Item.Channel; }
+        }
+
+        public PaasItem Item {
+            get {
+                if (item == null && track.ExternalId > 0) {
+                    item = PaasItem.Provider.FetchSingle (track.ExternalId);
+                }
+
+                return item ?? PaasItem.Empty;
+            }
+
+            set {
+                item = value;
+                track.ExternalId = value.DbId;
+            }
+        }
+
+        public DateTime PubDate {
+            get { return Item.PubDate; }
+        }
+
+        public string Description {
+            get { return Item.StrippedDescription; }
+        }
+
+        public bool IsNew {
+            get { return IsDownloaded && Item.IsNew; }
+        }
+
+        public bool IsDownloaded {
+            get { return !String.IsNullOrEmpty (Item.LocalPath); }
+        }
+
+        public int Position {
+            get { return position; }
+            set { position = value; }
+        }
+
+        public DateTime ReleaseDate {
+            get { return Item.PubDate; }
+        }
+
+        public PaasTrackInfo (DatabaseTrackInfo track) : base ()
+        {
+            this.track = track;
+        }
+
+        public PaasTrackInfo (DatabaseTrackInfo track, PaasItem item) : this (track)
+        {
+            Item = item;
+            SyncWithItem ();
+        }
+
+        static PaasTrackInfo ()
+        {
+            TrackInfo.PlaybackFinished += OnPlaybackFinished;
+        }
+
+        private static void OnPlaybackFinished (TrackInfo track, double percentCompleted)
+        {
+            if (percentCompleted > 0.5 && track.PlayCount > 0) {
+                PaasTrackInfo pi = PaasTrackInfo.From (track);
+                if (pi != null && pi.Item != PaasItem.Empty && pi.Item.IsNew) {
+                    pi.Item.IsNew = false;
+                    pi.Item.Save ();
+                }
+            }
+        }
+
+        public void SyncWithItem ()
+        {
+            PaasItem item = Item;
+
+            if (item == null || item == PaasItem.Empty || item.Channel == PaasChannel.Empty) {
+                return;
+            }
+
+            if (track.ExternalId != item.DbId) {
+                throw new Exception (String.Format (
+                    "PLEASE REPORT!  Track.TrackID:  {0} - Track.ExternalID:  {1} - Track.CacheEntryID:  {2} - Item.DbId = {3} - Item.CacheEntryID = {4}",
+                    track.TrackId, track.ExternalId, track.CacheEntryId, item.DbId, item.CacheEntryId
+                ));
+            }
+
+            track.ArtistName = item.Channel.Publisher;
+            track.AlbumTitle = item.Channel.Name;
+            track.TrackTitle = item.Name;
+            track.Year = item.PubDate.Year;
+            track.CanPlay = true;
+            track.Genre = track.Genre ?? "Podcast";
+            track.ReleaseDate = item.PubDate;
+            track.MimeType = item.MimeType;
+            track.Duration = item.Duration;
+            track.FileSize = item.Size;
+            track.LicenseUri = item.Channel.License;
+
+            track.Uri = new Banshee.Base.SafeUri (item.LocalPath ?? item.Url);
+
+            if (!String.IsNullOrEmpty (item.LocalPath)) {
+                try {
+                    TagLib.File file = Banshee.Streaming.StreamTagger.ProcessUri (track.Uri);
+                    Banshee.Streaming.StreamTagger.TrackInfoMerge (track, file, true);
+                } catch {}
+            }
+
+            track.MediaAttributes |= TrackMediaAttributes.Podcast;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasTrackListModel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasTrackListModel.cs
new file mode 100644
index 0000000..df55523
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasTrackListModel.cs
@@ -0,0 +1,100 @@
+//
+// PaasTrackListModel.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Hyena.Data.Sqlite;
+
+using Banshee.Sources;
+using Banshee.Database;
+
+using Banshee.Collection;
+using Banshee.Collection.Database;
+
+using Banshee.Paas.Gui;
+
+namespace Banshee.Paas.Data
+{
+    public class PaasTrackListModel : DatabaseTrackListModel
+    {
+        public PaasTrackListModel (BansheeDbConnection conn, IDatabaseTrackModelProvider provider, DatabaseSource source) : base (conn, provider, source)
+        {
+            From = String.Format ("{0}, {1}, {2}", provider.From, PaasChannel.Provider.TableName, PaasItem.Provider.TableName);
+
+            AddCondition (From, String.Format (
+                @"{1}.ID = CoreTracks.ExternalID AND
+                  {0}.ID = {1}.ChannelID         AND
+                  {1}.Active = 1",
+                PaasChannel.Provider.TableName, PaasItem.Provider.TableName
+            ));
+        }
+
+        protected override void GenerateSortQueryPart ()
+        {
+            SortQuery = (SortColumn == null)
+                ? GetSort ("album", true)
+                : GetSort (SortColumn.SortKey, SortColumn.SortType == Hyena.Data.SortType.Ascending);
+        }
+
+        public override void UpdateUnfilteredAggregates ()
+        {
+            HyenaSqliteCommand count_command = new HyenaSqliteCommand (String.Format (
+                "SELECT COUNT(*) {0} AND PaasItems.DownloadedAt NOT NULL", UnfilteredQuery
+            ));
+
+            UnfilteredCount = Connection.Query<int> (count_command);
+        }
+
+        public static string GetSort (string key, bool asc)
+        {
+            string ascDesc = asc ? "ASC" : "DESC";
+            string sort_query = null;
+
+            switch (key) {
+                case "PubDate":
+                    sort_query = String.Format ("PaasItems.PubDate {0}", ascDesc);
+                    break;
+                case "IsNew":
+                    sort_query = String.Format ("-PaasItems.IsNew {0}, PaasItems.PubDate DESC", ascDesc);
+                    break;
+                case "IsDownloaded":
+                    sort_query = String.Format (@"
+                        PaasItems.LocalPath IS NOT NULL {0}, PaasItems.PubDate DESC", ascDesc);
+                    break;
+                case "album":
+                    sort_query = String.Format (
+                        "HYENA_COLLATION_KEY(PaasChannels.Name) {0}, PaasItems.PubDate DESC", ascDesc
+                    );
+                    break;
+            }
+
+            return sort_query ?? Banshee.Query.BansheeQuery.GetSort (key, asc);
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasUnheardFilterModel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasUnheardFilterModel.cs
new file mode 100644
index 0000000..c67a627
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/PaasUnheardFilterModel.cs
@@ -0,0 +1,85 @@
+//
+// PaasUnheardFilterModel.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using Banshee.Collection;
+using Banshee.Collection.Database;
+
+namespace Banshee.Paas.Data
+{
+    public enum OldNewFilter
+    {
+        Both,
+        New,
+        Old
+    }
+
+    public class PaasUnheardFilterModel : FilterListModel<OldNewFilter>
+    {
+        public PaasUnheardFilterModel (DatabaseTrackListModel trackModel) : base (trackModel)
+        {
+            Selection.Clear (false);
+            Selection.QuietSelect (0);
+        }
+
+        public override void Reload (bool notify)
+        {
+            if (notify)
+                OnReloaded ();
+        }
+
+        public override void Clear ()
+        {
+        }
+
+        public override OldNewFilter this [int index] {
+            get {
+                switch (index) {
+                    case 1:    return OldNewFilter.Old;
+                    case 2:    return OldNewFilter.New;
+                    case 0:
+                    default:   return OldNewFilter.Both;
+                }
+            }
+        }
+
+        public override int Count {
+            get { return 3; }
+        }
+
+        public override string GetSqlFilter ()
+        {
+            if (Selection.AllSelected) {
+                return null;
+            } else if (Selection.Contains (2)) {
+                return "PaasItems.IsNew = 1";
+            } else {
+                return "PaasItems.IsNew = 0";
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/SingletonSelection.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/SingletonSelection.cs
new file mode 100644
index 0000000..4d0ef7a
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Data/SingletonSelection.cs
@@ -0,0 +1,47 @@
+//
+// SingletonSelection.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using  Hyena.Collections;
+
+namespace Banshee.Paas.Data
+{
+    public class SingletonSelection : Selection
+    {
+        public SingletonSelection ()
+        {
+        }
+
+        protected override void OnChanged ()
+        {
+            int fi = FirstIndex;
+            Clear (false);
+            QuietSelect (fi);
+            base.OnChanged ();
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Data/QueuedDownloadTask.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Data/QueuedDownloadTask.cs
new file mode 100644
index 0000000..351ed81
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Data/QueuedDownloadTask.cs
@@ -0,0 +1,86 @@
+//
+// QueuedDownloadTask.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Hyena;
+using Hyena.Data;
+using Hyena.Data.Sqlite;
+
+using Banshee.ServiceStack;
+using Banshee.Collection.Database;
+
+namespace Banshee.Paas.DownloadManager.Data
+{
+    public class QueuedDownloadTaskProvider : SqliteModelProvider<QueuedDownloadTask>
+    {
+        public QueuedDownloadTaskProvider (HyenaSqliteConnection connection) : base (connection, "QueuedDownloadTasks")
+        {
+        }
+    }
+
+    public class QueuedDownloadTask
+    {
+        private static QueuedDownloadTaskProvider provider;
+
+        public static QueuedDownloadTaskProvider Provider {
+            get { return provider; }
+        }
+
+        static QueuedDownloadTask () {
+            provider = new QueuedDownloadTaskProvider (ServiceManager.DbConnection);
+        }
+
+        private long dbid;
+        [DatabaseColumn ("ID", Constraints = DatabaseColumnConstraints.PrimaryKey)]
+        public long DbId {
+            get { return dbid; }
+            protected set { dbid = value; }
+        }
+
+        private long primary_source_id;
+        [DatabaseColumn ("PrimarySourceID", Index = "QueuedDownloadTaskPrimarySourceIDIndex")]
+        public long PrimarySourceID {
+            get { return primary_source_id; }
+            set { primary_source_id = value; }
+        }
+
+        private long external_id;
+        [DatabaseColumn ("ExternalID", Index = "QueuedDownloadTaskExternalIDIndex")]
+        public long ExternalID {
+            get { return external_id; }
+            set { external_id = value; }
+        }
+
+        private long position;
+        [DatabaseColumn ("Position", Index = "QueuedDownloadTaskPositionIndex")]
+        public long Position {
+            get { return position; }
+            set { position = value; }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadListView.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadListView.cs
new file mode 100644
index 0000000..c624dca
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadListView.cs
@@ -0,0 +1,125 @@
+//
+// DownloadListView.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Text;
+
+using Gtk;
+using Mono.Unix;
+
+using Hyena.Data.Gui;
+
+using Migo2.DownloadService;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+using Banshee.Collection.Gui;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.DownloadManager.Gui
+{
+    public class DownloadListView : ListView<HttpFileDownloadTask>
+    {
+        private PaasDownloadManager manager;
+
+        public DownloadListView (PaasDownloadManager manager)
+        {
+            this.manager = manager;
+
+            IsReorderable = true;
+            IsEverReorderable = true;
+
+            ColumnCellText name_renderer = new ColumnCellText ("Name", true);
+            ColumnCellText progress_renderer = new ColumnCellText ("Progress", true);
+
+            ColumnController = new ColumnController ();
+
+            ColumnController.Add (new Column (Catalog.GetString ("Downloads"), name_renderer, 0.7));
+            ColumnController.Add (new Column (Catalog.GetString ("Progress"), progress_renderer, 0.3));
+
+            //ColumnController  = column_controller;
+            //RowHeightProvider = renderer.ComputeRowHeight;
+        }
+/*
+        protected override bool OnPopupMenu ()
+        {
+            ServiceManager.Get<InterfaceActionService> ().FindAction ("Paas.PaasChannelPopupAction").Activate ();
+            return true;
+        }
+*/
+
+#region D&D
+//    Dragon-Drop
+
+//                     _ _
+//              _     //` `\
+//          _,-"\%   // /``\`\
+//     ~^~ >__^  |% // /  } `\`\
+//            )  )%// / }  } }`\`\
+//           /  (%/'/.\_/\_/\_/\`/
+//          (    '         `-._`
+//           \   ,     (  \   _`-.__.-;%>
+//          /_`\ \      `\ \." `-..-'`
+//         ``` /_/`"-=-'`/_/
+//
+        protected override void OnDragSourceSet ()
+        {
+            base.OnDragSourceSet ();
+        }
+
+        protected override bool OnDragDrop (Gdk.DragContext context, int x, int y, uint time_)
+        {
+            y = TranslateToListY (y);
+
+            if (Gtk.Drag.GetSourceWidget (context) == this) {
+                DownloadSource source = ServiceManager.SourceManager.ActiveSource as DownloadSource;
+
+                if (source != null) {
+                    int row = GetRowAtY (y);
+                    DownloadListModel model = source.DownloadListModel;
+
+                    if (row != GetRowAtY (y + RowHeight / 2)) {
+                        row += 1;
+                    }
+
+                    if (model.Selection.Contains (row)) {
+                        return false;
+                    }
+
+                    try {
+                        manager.Move (row, model.GetSelected ());
+                    } catch {}
+
+                    return true;
+                }
+            }
+
+            return false;
+        }
+#endregion
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadManagerInterface.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadManagerInterface.cs
new file mode 100644
index 0000000..170bb48
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadManagerInterface.cs
@@ -0,0 +1,191 @@
+//
+// DownloadManagerInterface.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.ComponentModel;
+
+using Gtk;
+
+using Migo2.Async;
+using Migo2.DownloadService;
+
+using Banshee.Base;
+using Banshee.ServiceStack;
+
+using Banshee.Paas.Gui;
+
+namespace Banshee.Paas.DownloadManager.Gui
+{
+    public class DownloadManagerInterface : IDisposable
+    {
+        private bool cancelled;
+        private PaasSource source;
+        private PaasDownloadManager manager;
+        private DownloadUserJob downloadJob;
+
+        private DownloadSource downloadSource;
+        private DownloadListModel download_model;
+
+        public DownloadManagerInterface (PaasSource source, PaasDownloadManager manager)
+        {
+            if (manager == null) {
+                throw new ArgumentNullException ("manager");
+            }
+
+            this.source = source;
+            this.manager = manager;
+
+            download_model = new DownloadListModel ();
+            DownloadListView download_view = new DownloadListView (manager);
+
+            downloadSource = new DownloadSource (download_model, download_view);
+
+            manager.Started += OnManagerStartedHandler;
+            manager.Stopped += OnManagerStoppedHandler;
+            manager.ProgressChanged += OnManagerProgressChangedHandler;
+            manager.StatusChanged += OnManagerStatusChangedHandler;
+
+            manager.TaskAdded += OnManagerTaskAddedHandler;
+            manager.TaskProgressChanged += (sender, e) => {
+                ThreadAssist.ProxyToMain (delegate {
+                    if (cancelled) {
+                        return;
+                    }
+
+                    download_model.Reload ();
+                });
+            };
+
+            manager.TaskCompleted += (sender, e) => {
+                ThreadAssist.ProxyToMain (delegate {
+                    if (e.Task.State != TaskState.Paused) {
+                        lock (this) {
+                            if (cancelled) {
+                                return;
+                            }
+                        }
+
+                        download_model.Remove (e.Task);
+                    }
+                });
+            };
+
+            manager.Reordered += (sender, e) => {
+                ThreadAssist.ProxyToMain (delegate {
+                    download_model.Reorder (e.NewOrder);
+                });
+            };
+        }
+
+        public void Dispose ()
+        {
+            ThreadAssist.AssertInMainThread ();
+
+            if (manager != null) {
+                manager.Started -= OnManagerStartedHandler;
+                manager.Stopped -= OnManagerStoppedHandler;
+                manager.ProgressChanged -= OnManagerProgressChangedHandler;
+                manager.StatusChanged -= OnManagerStatusChangedHandler;
+
+                manager = null;
+            }
+
+            if (downloadJob != null) {
+                downloadJob.CancelRequested -= OnCancelRequested;
+                downloadJob.Finish ();
+                downloadJob = null;
+                source.RemoveChildSource (downloadSource);
+                downloadSource = null;
+            }
+        }
+
+        private void OnManagerStartedHandler (object sender, EventArgs e)
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                cancelled = false;
+
+                if (downloadJob == null) {
+                    source.AddChildSource (downloadSource);
+
+                    downloadJob = new DownloadUserJob ();
+                    downloadJob.CancelRequested += OnCancelRequested;
+                    downloadJob.Register ();
+                }
+            });
+        }
+
+        private void OnManagerStoppedHandler (object sender, EventArgs e)
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                download_model.Clear ();
+
+                if (downloadJob != null) {
+                    downloadJob.CancelRequested -= OnCancelRequested;
+                    downloadJob.Finish ();
+                    downloadJob = null;
+                    source.RemoveChildSource (downloadSource);
+                }
+            });
+        }
+
+        private void OnManagerProgressChangedHandler (object sender, ProgressChangedEventArgs e)
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                if (downloadJob != null) {
+                    downloadJob.UpdateProgress (e.ProgressPercentage);
+                }
+            });
+        }
+
+        private void OnManagerStatusChangedHandler (object sender, GroupStatusChangedEventArgs e)
+        {
+            HttpDownloadGroupStatusChangedEventArgs args = e as HttpDownloadGroupStatusChangedEventArgs;
+
+            ThreadAssist.ProxyToMain (delegate {
+                if (downloadJob != null) {
+                    downloadJob.UpdateStatus (args.RunningTasks, args.RemainingTasks, args.CompletedTasks, args.BytesPerSecond);
+                }
+            });
+        }
+
+        private void OnManagerTaskAddedHandler (object sender, TaskAddedEventArgs<HttpFileDownloadTask> e)
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                if (e.TaskPair != null) {
+                    download_model.AddTaskPair (e.TaskPair);
+                } else if (e.TaskPairs != null) {
+                    download_model.AddTaskPairs (e.TaskPairs);
+                }
+            });
+        }
+
+        private void OnCancelRequested (object sender, EventArgs e)
+        {
+            cancelled = true;
+            manager.CancelAsync ();
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadSource.cs
new file mode 100644
index 0000000..53aee1e
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadSource.cs
@@ -0,0 +1,92 @@
+//
+// DownloadSource.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//   Aaron Bockover <abockover novell com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+// Copyright (C) 2007 Novell, Inc. (ErrorSource.cs)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Hyena.Data;
+using Hyena.Collections;
+
+using Banshee.Base;
+using Banshee.Sources;
+using Banshee.Sources.Gui;
+
+using Migo2.Async;
+using Migo2.Collections;
+using Migo2.DownloadService;
+
+namespace Banshee.Paas.DownloadManager.Gui
+{
+    public class DownloadSource : Source
+    {
+        private ISourceContents contents;
+        private DownloadListModel download_model;
+
+        public DownloadListModel DownloadListModel
+        {
+            get { return download_model; }
+        }
+
+        public DownloadSource (DownloadListModel model, DownloadListView view) : base (Catalog.GetString ("Downloads"), Catalog.GetString ("Downloads"), 0)
+        {
+            if (model == null) {
+                throw new ArgumentNullException ("model");
+            }
+
+            TypeUniqueId = "DownloadSource";
+
+            download_model = model;
+            download_model.Cleared  += (sender, e) => { OnUpdated (); };
+            download_model.Reloaded += (sender, e) => { QueueDraw (); OnUpdated (); };
+
+            Properties.SetStringList ("Icon.Name", "gtk-network", "network");
+
+            contents = new DownloadSourceContents (view);
+            Properties.Set<ISourceContents> ("Nereid.SourceContents", contents);
+            Properties.Set<bool> ("Nereid.SourceContentsPropagate", false);
+        }
+
+        public override void Activate ()
+        {
+            download_model.Reload ();
+        }
+
+        public void QueueDraw ()
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                contents.Widget.QueueDraw ();
+            });
+        }
+
+        public override int Count {
+            get { return download_model.Count; }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadSourceContents.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadSourceContents.cs
new file mode 100644
index 0000000..fe24301
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadSourceContents.cs
@@ -0,0 +1,81 @@
+//
+// DownloadSourceContents.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Gtk;
+
+using Banshee.Sources;
+using Banshee.Sources.Gui;
+
+namespace Banshee.Paas.DownloadManager.Gui
+{
+    public class DownloadSourceContents : ISourceContents
+    {
+        private ScrolledWindow sw;
+        private DownloadSource download_source;
+        private DownloadListView download_view;
+
+        public ISource Source {
+            get { return download_source; }
+        }
+
+        public Widget Widget {
+            get { return sw as Widget; }
+        }
+
+        public DownloadSourceContents (DownloadListView view)
+        {
+            download_view = view;
+
+            sw = new ScrolledWindow ();
+            sw.Add (download_view);
+            sw.ShowAll ();
+        }
+
+        public bool SetSource (ISource source)
+        {
+            download_source = source as DownloadSource;
+
+            if (download_source == null) {
+                return false;
+            }
+
+            download_view.HeaderVisible = true;
+            download_view.SetModel (download_source.DownloadListModel);
+
+            return true;
+        }
+
+        public void ResetSource ()
+        {
+            download_source = null;
+
+            download_view.SetModel (null);
+            download_view.HeaderVisible = false;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadUserJob.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadUserJob.cs
new file mode 100644
index 0000000..d6c4ac7
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadUserJob.cs
@@ -0,0 +1,147 @@
+//
+// DownloadUserJob.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Gtk;
+using Mono.Unix;
+
+using Migo2.Utils;
+
+using Banshee.ServiceStack;
+
+namespace Banshee.Paas.DownloadManager.Gui
+{
+    public class DownloadUserJob : UserJob
+    {
+        private bool disposed = false;
+        private bool canceled = false;
+        private bool cancelRequested = false;
+
+        private readonly object sync = new object ();
+
+        public DownloadUserJob () : base (Catalog.GetString ("Downloads"), String.Empty, String.Empty)
+        {
+            CancelRequested += OnCancelRequested;
+
+            Title = Catalog.GetString ("Downloading");
+            Status = Catalog.GetString ("Initializing...");
+            CancelMessage = Catalog.GetString ("Cancel all downloads?");
+
+            this.IconNames = new string[1] { Stock.Network };
+
+            CanCancel = true;
+        }
+
+        public void Dispose ()
+        {
+            lock (sync) {
+                if (disposed) {
+                    throw new ObjectDisposedException (GetType ().FullName);
+                } else if (cancelRequested) {
+                    throw new InvalidOperationException ("Cannot dispose object while canceling.");
+                } else {
+                    disposed = true;
+                }
+
+                CancelRequested -= OnCancelRequested;
+            }
+        }
+
+        private bool SetCanceled ()
+        {
+            bool ret = false;
+
+            lock (sync) {
+                if (!cancelRequested && !canceled && !disposed) {
+                    CanCancel = false;
+                    ret = cancelRequested = true;
+                }
+            }
+
+            return ret;
+        }
+
+        public void UpdateProgress (int progress)
+        {
+            if (progress < 0 || progress > 100) {
+                throw new ArgumentException ("progress:  Must be between 0 and 100.");
+            }
+
+            lock (sync) {
+                if (canceled || cancelRequested || disposed) {
+                    return;
+                }
+
+                Progress = (double) progress / 100;
+            }
+        }
+
+        public void UpdateStatus (int downloading, int remaining, int completed, long bytesPerSecond)
+        {
+            if (downloading < 0) {
+                throw new ArgumentException ("Must be positive.", "downloading");
+            } else if (bytesPerSecond < 0) {
+                bytesPerSecond = 0;
+            }
+
+            lock (sync) {
+                if (canceled || cancelRequested || disposed) {
+                    return;
+                }
+
+                string status;
+
+                if (downloading > 0) {
+                    status = Catalog.GetPluralString (
+                        "Currently transferring {0} file at {1}/s",
+                        "Currently transferring {0} files at {1}/s", downloading
+                    );
+                } else {
+                    status = Catalog.GetString ("All downloads are currently idle");
+                }
+
+                Status = String.Format (status, downloading, UnitUtils.ToString (bytesPerSecond));
+            }
+        }
+
+        private void OnCancelRequested (object sender, EventArgs e)
+        {
+            if (SetCanceled ()) {
+                lock (sync)  {
+                    Progress = 0.0;
+                    Title = Catalog.GetString ("Canceling Downloads");
+                    Status = Catalog.GetString (
+                        "Waiting for downloads to terminate..."
+                    );
+
+                    cancelRequested = false;
+                    canceled = true;
+                }
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager/DownloadListModel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager/DownloadListModel.cs
new file mode 100644
index 0000000..2f3a993
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager/DownloadListModel.cs
@@ -0,0 +1,74 @@
+//
+// DownloadListModel.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Hyena.Data;
+using Hyena.Collections;
+
+using Migo2.Async;
+using Migo2.Collections;
+using Migo2.DownloadService;
+
+using Banshee.Paas.Utils;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.DownloadManager
+{
+    public class DownloadListModel : ListModel<HttpFileDownloadTask>
+    {
+        public override bool CanReorder {
+            get { return true; }
+        }
+
+        public void AddTaskPair (Pair<int,HttpFileDownloadTask> pair)
+        {
+            if (pair != null && pair.Second != null) {
+                Items.Insert (pair.First, pair.Second);
+            }
+
+            OnReloaded ();
+        }
+
+        public void AddTaskPairs (IEnumerable<Pair<int,HttpFileDownloadTask>> pairs)
+        {
+            if (pairs != null) {
+                foreach (Pair<int,HttpFileDownloadTask> pair in pairs) {
+                    if (pair != null && pair.Second != null) {
+                        Items.Insert (pair.First, pair.Second);
+                    }
+                }
+            }
+
+            OnReloaded ();
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager/PaasDownloadManager.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager/PaasDownloadManager.cs
new file mode 100644
index 0000000..3816a61
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.DownloadManager/PaasDownloadManager.cs
@@ -0,0 +1,365 @@
+//
+// PaasDownloadManager.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+using Hyena.Collections;
+
+using Migo2.Async;
+using Migo2.Collections;
+using Migo2.DownloadService;
+
+using Banshee.ServiceStack;
+
+using Banshee.Paas.Data;
+using Banshee.Paas.DownloadManager.Data;
+
+namespace Banshee.Paas.DownloadManager
+{
+    public class PaasDownloadManager : HttpDownloadManager
+    {
+        long primary_source_id;
+        private Dictionary<long,HttpFileDownloadTask> downloads;
+
+        public PaasDownloadManager (long primarySourceID, int maxDownloads, string tmpDir) : base (maxDownloads, tmpDir)
+        {
+            primary_source_id = primarySourceID;
+            downloads = new Dictionary<long,HttpFileDownloadTask> ();
+
+            TaskCompleted += (sender, e) => {
+                if (e.Task.State != TaskState.Paused) {
+                    lock (SyncRoot) {
+                        downloads.Remove ((e.Task.UserState as PaasItem).DbId);
+                    }
+                }
+            };
+        }
+
+        public override void CancelAsync ()
+        {
+            lock (SyncRoot) {
+                base.CancelAsync ();
+                ClearQueuedDownloads ();
+            }
+        }
+
+        public TaskState CheckActiveDownloadStatus (PaasItem item)
+        {
+            return CheckActiveDownloadStatus (item.DbId);
+        }
+
+        public TaskState CheckActiveDownloadStatus (long itemID)
+        {
+            lock (SyncRoot) {
+                if (downloads.ContainsKey (itemID)) {
+                    return downloads[itemID].State;
+                }
+
+                return TaskState.None;
+            }
+        }
+
+        public bool Contains (PaasItem item)
+        {
+            return Contains (item.DbId);
+        }
+
+        public bool Contains (long itemID)
+        {
+            lock (SyncRoot) {
+                return downloads.ContainsKey (itemID);
+            }
+        }
+
+        public void QueueDownload (PaasItem item)
+        {
+            lock (SyncRoot) {
+                if (!IsDisposing) {
+                    if (item != null && item.Active && !downloads.ContainsKey (item.DbId)) {
+                        HttpFileDownloadTask task = CreateDownloadTask (item.Url, item);
+                        task.Name = String.Format ("{0} - {1}", item.Channel.Name, item.Name);
+
+                        downloads.Add (item.DbId, task);
+                        try {
+                            Add (task);
+                        } catch { downloads.Remove (item.DbId); }
+                    }
+                }
+            }
+        }
+
+        public void QueueDownload (IEnumerable<PaasItem> items)
+        {
+            lock (SyncRoot) {
+                if (!IsDisposing) {
+                    HttpFileDownloadTask task = null;
+                    List<HttpFileDownloadTask> tasks = new List<HttpFileDownloadTask> ();
+
+                    foreach (PaasItem item in items.Where (i => i.Active && !i.IsDownloaded && !downloads.ContainsKey (i.DbId))) {
+                        task = CreateDownloadTask (item.Url, item);
+                        task.Name = String.Format ("{0} - {1}", item.Channel.Name, item.Name);
+
+                        tasks.Add (task);
+                        downloads.Add (item.DbId, task);
+                    }
+
+                    try {
+                        if (tasks.Count > 0) {
+                            Add (tasks);
+                        }
+                    } catch {
+                        foreach (var t in tasks) {
+                            downloads.Remove (((PaasItem)t.UserState).DbId);
+                        }
+                    }
+                }
+            }
+        }
+
+        public void CancelDownload (PaasItem item)
+        {
+            lock (SyncRoot) {
+                if (!IsDisposing) {
+                    if (downloads.ContainsKey (item.DbId)) {
+                        downloads[item.DbId].CancelAsync ();
+                        //RemoveQueuedDownload (item.DbId);
+                    }
+                }
+            }
+        }
+
+        public void CancelDownload (IEnumerable<PaasItem> items)
+        {
+            lock (SyncRoot) {
+                if (!IsDisposing) {
+                    //RangeCollection rc = new RangeCollection ();
+
+                    foreach (PaasItem item in items) {
+                        if (downloads.ContainsKey (item.DbId)) {
+                            //rc.Add ((int)item.DbId);
+                            downloads[item.DbId].CancelAsync ();
+                        }
+                    }
+
+                    //RemoveQueuedDownloadRange (rc);
+                }
+            }
+        }
+
+        public void PauseDownload (PaasItem item)
+        {
+            lock (SyncRoot) {
+                if (!IsDisposing) {
+                    if (downloads.ContainsKey (item.DbId)) {
+                        downloads[item.DbId].PauseAsync ();
+                    }
+                }
+            }
+        }
+
+        public void PauseDownload (IEnumerable<PaasItem> items)
+        {
+            lock (SyncRoot) {
+                if (!IsDisposing) {
+                    foreach (PaasItem item in items) {
+                        if (downloads.ContainsKey (item.DbId)) {
+                            downloads[item.DbId].PauseAsync ();
+                        }
+                    }
+                }
+            }
+        }
+
+        public void ResumeDownload (PaasItem item)
+        {
+            lock (SyncRoot) {
+                if (!IsDisposing) {
+                    if (downloads.ContainsKey (item.DbId)) {
+                        downloads[item.DbId].ResumeAsync ();
+                    }
+                }
+            }
+        }
+
+        public void ResumeDownload (IEnumerable<PaasItem> items)
+        {
+            lock (SyncRoot) {
+                if (!IsDisposing) {
+                    foreach (PaasItem item in items) {
+                        if (downloads.ContainsKey (item.DbId)) {
+                            downloads[item.DbId].ResumeAsync ();
+                        }
+                    }
+                }
+            }
+        }
+
+        public void RestoreQueuedDownloads ()
+        {
+            lock (SyncRoot) {
+                if (IsDisposing) {
+                    return;
+                }
+
+                QueueDownload (PaasItem.Provider.FetchQueued (primary_source_id));
+            }
+        }
+
+        private void SaveQueuedDownloads ()
+        {
+            lock (SyncRoot) {
+                ServiceManager.DbConnection.BeginTransaction ();
+
+                try {
+                    ClearQueuedDownloads ();
+
+                    int i = 0;
+                    foreach (var task in Tasks) {
+                        AddQueuedDownload ((task.UserState as PaasItem).DbId, i++);
+                    }
+
+                    ServiceManager.DbConnection.CommitTransaction ();
+                } catch {
+                    ServiceManager.DbConnection.RollbackTransaction ();
+                    throw;
+                }
+            }
+        }
+
+        private void ClearQueuedDownloads ()
+        {
+            ServiceManager.DbConnection.Execute (
+                String.Format (
+                    "DELETE FROM {0} WHERE PrimarySourceID = ?",
+                    QueuedDownloadTask.Provider.From
+                ), primary_source_id
+            );
+        }
+
+        private void AddQueuedDownload (long item_id, long position)
+        {
+            QueuedDownloadTask.Provider.Save (new QueuedDownloadTask () {
+                PrimarySourceID = primary_source_id,
+                ExternalID = item_id,
+                Position = position
+            });
+        }
+
+//        private void AddQueuedDownloads (IEnumerable<Pair<int, HttpFileDownloadTask>> pairs)
+//        {
+//            PaasItem item = null;
+//            ServiceManager.DbConnection.BeginTransaction ();
+//
+//            try {
+//                foreach (Pair<int,HttpFileDownloadTask> pair in pairs) {
+//                    item = pair.Second.UserState as PaasItem;
+//
+//                    if (item != null) {
+//                        AddQueuedDownload (item.DbId, (int)pair.First);
+//                    }
+//                }
+//
+//                ServiceManager.DbConnection.CommitTransaction ();
+//            } catch {
+//                ServiceManager.DbConnection.RollbackTransaction ();
+//                throw;
+//            }
+//        }
+
+//        private void RemoveQueuedDownload (long item_id)
+//        {
+//            ServiceManager.DbConnection.Execute (
+//                String.Format (
+//                    "DELETE FROM {0} WHERE PrimarySourceID = ? AND ExternalID = ?",
+//                    QueuedDownloadTask.Provider.From
+//                ), primary_source_id, item_id
+//            );
+//        }
+//
+//        private void RemoveQueuedDownloadRange (IEnumerable<int> ids)
+//        {
+//            RangeCollection rc = new RangeCollection ();
+//
+//            foreach (int i in ids) {
+//                rc.Add (i);
+//            }
+//
+//            RemoveQueuedDownloadRange (rc);
+//        }
+//
+//        private void RemoveQueuedDownloadRange (RangeCollection collection)
+//        {
+//            ServiceManager.DbConnection.BeginTransaction ();
+//
+//            try {
+//                foreach (RangeCollection.Range range in collection.Ranges) {
+//                    ServiceManager.DbConnection.Execute (
+//                        String.Format (
+//                            "DELETE FROM {0} WHERE PrimarySourceID = ? AND ExternalID >= ? AND ExternalID <= ?",
+//                            QueuedDownloadTask.Provider.From
+//                        ), primary_source_id, range.Start, range.End
+//                    );
+//                }
+//
+//                ServiceManager.DbConnection.CommitTransaction ();
+//            } catch {
+//                ServiceManager.DbConnection.RollbackTransaction ();
+//                throw;
+//            }
+//        }
+
+        public override void Dispose ()
+        {
+            if (SetDisposing ()) {
+                SaveQueuedDownloads ();
+                base.Dispose ();
+            }
+        }
+
+//        protected override void OnTaskAdded (int pos, HttpFileDownloadTask task)
+//        {
+//            AddQueuedDownload ((task.UserState as PaasItem).DbId, pos);
+//            base.OnTaskAdded (pos, task);
+//        }
+//
+//        protected override void OnTasksAdded (ICollection<Migo2.Collections.Pair<int, HttpFileDownloadTask>> pairs)
+//        {
+//            AddQueuedDownloads (pairs);
+//            base.OnTasksAdded (pairs);
+//        }
+
+//        protected override void OnTaskCompleted (HttpFileDownloadTask task, TaskCompletedEventArgs e)
+//        {
+//            if (e.State == TaskState.Succeeded) {
+//                RemoveQueuedDownload ((task.UserState as PaasItem).DbId);
+//            }
+//
+//            base.OnTaskCompleted (task, e);
+//        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellChannel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellChannel.cs
new file mode 100644
index 0000000..72ac206
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellChannel.cs
@@ -0,0 +1,197 @@
+//
+// ColumnCellChannel.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Gtk;
+using Cairo;
+
+using Mono.Unix;
+
+using Hyena.Gui;
+using Hyena.Gui.Theming;
+using Hyena.Data.Gui;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+using Banshee.Collection.Gui;
+
+using Banshee.Paas.Data;
+using Banshee.Paas.Aether;
+
+namespace Banshee.Paas.Gui
+{
+    public class ColumnCellChannel : ColumnCell, IColumnCellDataHelper
+    {
+        private static int image_spacing = 4;
+        private static int image_size = 48;
+
+        // TODO replace this w/ new icon installation etc
+        private static ImageSurface default_cover_image = new PixbufImageSurface (
+            IconThemeUtils.LoadIcon (image_size, "podcast")
+        );
+
+        private static ImageSurface updating_image = PixbufImageSurface.Create (
+            IconThemeUtils.LoadIcon (image_size, Stock.Refresh), true
+        );
+
+        private ArtworkManager artwork_manager;
+
+        public ColumnCellDataHelper DataHelper { get; set; }
+
+        public ColumnCellChannel () : base (null, true)
+        {
+            artwork_manager = ServiceManager.Get<ArtworkManager> ();
+        }
+
+        public override void Render (CellContext context, StateType state, double cellWidth, double cellHeight)
+        {
+            if (BoundObject == null) {
+                return;
+            }
+
+            if (!(BoundObject is PaasChannel)) {
+                throw new InvalidCastException("ColumnCellChannel can only bind to PaasChannel objects");
+            }
+
+            PaasChannel channel = (PaasChannel)BoundObject;
+
+            bool disable_border = false;
+
+            ImageSurface image = (artwork_manager == null) ? null
+                : artwork_manager.LookupScaleSurface (PaasService.ArtworkIdFor (channel), image_size, true);
+
+            bool waiting = false;
+
+            if (DataHelper != null) {
+                switch ((ChannelUpdateStatus)DataHelper (this, channel)) {
+                case ChannelUpdateStatus.Waiting:
+                    waiting = true;
+                    goto case ChannelUpdateStatus.Updating;
+                case ChannelUpdateStatus.Updating:
+                    image = updating_image;
+                    disable_border = true;
+                    break;
+                }
+            }
+
+            if (image == null) {
+                image = default_cover_image;
+                disable_border = true;
+            }
+
+            // int image_render_size = is_default ? image.Height : (int)cellHeight - 8;
+            int image_render_size = image_size;
+            int x = image_spacing;
+            int y = ((int)cellHeight - image_render_size) / 2;
+
+            ArtworkRenderer.RenderThumbnail (context.Context, image, false, x, y,
+                image_render_size, image_render_size, !disable_border, context.Theme.Context.Radius, !waiting
+            );
+
+            int fl_width = 0, fl_height = 0, sl_width = 0, sl_height = 0;
+            Cairo.Color text_color = context.Theme.Colors.GetWidgetColor (GtkColorClass.Text, state);
+            text_color.A = 0.75;
+
+            Pango.Layout layout = context.Layout;
+            layout.Width = (int)((cellWidth - cellHeight - x - 10) * Pango.Scale.PangoScale);
+            layout.Ellipsize = Pango.EllipsizeMode.End;
+            layout.FontDescription.Weight = Pango.Weight.Bold;
+
+            // Compute the layout sizes for both lines for centering on the cell
+            int old_size = layout.FontDescription.Size;
+
+            layout.SetText (channel.Name ?? String.Empty);
+            layout.GetPixelSize (out fl_width, out fl_height);
+
+            if (channel.DbId > 0) {
+                layout.FontDescription.Weight = Pango.Weight.Normal;
+                layout.FontDescription.Size = (int)(old_size * Pango.Scale.Small);
+                layout.FontDescription.Style = Pango.Style.Italic;
+
+                if (channel.LastDownloadTime == DateTime.MinValue) {
+                    layout.SetText (Catalog.GetString ("New!"));
+                } else if (channel.LastDownloadTime.Date == DateTime.Now.Date) {
+                    layout.SetText (String.Format (Catalog.GetString ("Last updated at {0}"), channel.LastDownloadTime.ToShortTimeString ()));
+                } else {
+                    layout.SetText (String.Format (Catalog.GetString ("Last updated on {0}"), channel.LastDownloadTime.ToLongDateString ()));
+                }
+
+                layout.GetPixelSize (out sl_width, out sl_height);
+            }
+
+            // Calculate the layout positioning
+            x = ((int)cellHeight - x) + 10;
+            y = (int)((cellHeight - (fl_height + sl_height)) / 2);
+
+            // Render the second line first since we have that state already
+            if (channel.DbId > 0) {
+                context.Context.MoveTo (x, y + fl_height);
+                context.Context.Color = text_color;
+                PangoCairoHelper.ShowLayout (context.Context, layout);
+            }
+
+            // Render the first line, resetting the state
+            //layout.SetText (channel.Name ?? String.Empty);
+            layout.FontDescription.Weight = Pango.Weight.Bold;
+            layout.FontDescription.Size = old_size;
+            layout.FontDescription.Style = Pango.Style.Normal;
+
+            layout.SetText (channel.Name ?? String.Empty);
+
+            context.Context.MoveTo (x, y);
+            text_color.A = 1;
+            context.Context.Color = text_color;
+            PangoCairoHelper.ShowLayout (context.Context, layout);
+        }
+
+        public int ComputeRowHeight (Widget widget)
+        {
+            int height;
+            int text_w, text_h;
+
+            Pango.Layout layout = new Pango.Layout (widget.PangoContext);
+            layout.FontDescription = widget.PangoContext.FontDescription.Copy ();
+
+            layout.FontDescription.Weight = Pango.Weight.Bold;
+            layout.SetText ("W");
+            layout.GetPixelSize (out text_w, out text_h);
+            height = text_h;
+
+            layout.FontDescription.Weight = Pango.Weight.Normal;
+            layout.FontDescription.Size = (int)(layout.FontDescription.Size * Pango.Scale.Small);
+            layout.FontDescription.Style = Pango.Style.Italic;
+            layout.SetText ("W");
+            layout.GetPixelSize (out text_w, out text_h);
+            height += text_h;
+
+            layout.Dispose ();
+
+            return (height < image_size ? image_size : height) + 6;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellDownloadStatus.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellDownloadStatus.cs
new file mode 100644
index 0000000..f84cc79
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellDownloadStatus.cs
@@ -0,0 +1,56 @@
+//
+// ColumnCellDownloadStatus.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+using Mono.Unix;
+
+using Hyena.Data.Gui;
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class ColumnCellDownloadStatus : ColumnCellText
+    {
+        public ColumnCellDownloadStatus () : base (null, true)
+        {
+        }
+
+        protected override string GetText (object obj)
+        {
+            DownloadedStatusFilter val = (DownloadedStatusFilter) obj;
+
+            switch (val) {
+                case DownloadedStatusFilter.Downloaded:    return Catalog.GetString ("Episodes I've Downloaded");
+                case DownloadedStatusFilter.Both:          return Catalog.GetString ("All");
+                case DownloadedStatusFilter.NotDownloaded: return Catalog.GetString ("Episodes I Haven't Downloaded");
+            }
+            return String.Empty;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellPaasStatusIndicator.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellPaasStatusIndicator.cs
new file mode 100644
index 0000000..4f93995
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellPaasStatusIndicator.cs
@@ -0,0 +1,123 @@
+//
+// ColumnCellPaasStatusIndicator.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Gtk;
+
+using Hyena.Data.Gui;
+
+using Banshee.Gui;
+using Banshee.Collection;
+using Banshee.Collection.Gui;
+
+using Migo2.Async;
+
+using Banshee.Paas;
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class ColumnCellPaasStatusIndicator : ColumnCellStatusIndicator, IColumnCellDataHelper
+    {
+        protected enum Offset : int {
+            New         = 0,
+            Downloading = 1,
+            Video       = 2,
+            Count       = 3,
+        }
+
+        private int  icon_index = -1;
+        private bool sensitive = true;
+
+        public ColumnCellDataHelper DataHelper { get; set; }
+
+        protected override int PixbufCount {
+            get { return base.PixbufCount + (int)Offset.Count; }
+        }
+
+        public ColumnCellPaasStatusIndicator (string property) : base (property)
+        {
+        }
+
+        public ColumnCellPaasStatusIndicator (string property, bool expand) : base (property, expand)
+        {
+        }
+
+        protected override void LoadPixbufs ()
+        {
+            base.LoadPixbufs ();
+
+            Pixbufs[base.PixbufCount + (int)Offset.New]         = IconThemeUtils.LoadIcon (PixbufSize, "podcast-new");
+            Pixbufs[base.PixbufCount + (int)Offset.Downloading] = IconThemeUtils.LoadIcon (PixbufSize, "document-save");
+            Pixbufs[base.PixbufCount + (int)Offset.Video]       = IconThemeUtils.LoadIcon (PixbufSize, "video-x-generic");
+        }
+
+        protected override int GetIconIndex (TrackInfo track)
+        {
+            return icon_index;
+        }
+
+        public override void Render (CellContext context, StateType state, double cellWidth, double cellHeight)
+        {
+            sensitive = true;
+
+            PaasTrackInfo pti = PaasTrackInfo.From (BoundTrack);
+            PaasItem item = (pti != null) ? pti.Item : null;
+
+            if (pti == null || item == null) {
+                icon_index = -1;
+            } else {
+                TaskState task_state = (DataHelper != null) ? (TaskState)DataHelper (this, item) : TaskState.None;
+
+                switch (task_state) {
+                    case TaskState.Ready:
+                        sensitive = pti.Track.IsPlaying;
+                        goto case TaskState.Running;
+                    case TaskState.Running:
+                        icon_index = base.PixbufCount + (int)Offset.Downloading;
+                        break;
+                    case TaskState.Paused:
+                        icon_index = (int)Icon.Paused;
+                        break;
+                    case TaskState.Failed:
+                        icon_index = (int)Icon.Error;
+                        break;
+                    default:
+                        if (item.Error) {
+                            icon_index = (int)Icon.Error;
+                        } else {
+                            icon_index = pti.IsNew ? base.PixbufCount + (int)Offset.New : -1;
+                        }
+
+                        break;
+                }
+            }
+
+            context.Opaque = sensitive;
+            base.Render (context, state, cellWidth, cellHeight);
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellPublished.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellPublished.cs
new file mode 100644
index 0000000..f7ecf17
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellPublished.cs
@@ -0,0 +1,41 @@
+//
+// ColumnCellPublished.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+namespace Banshee.Paas.Gui
+{
+    public class ColumnCellPublished : Banshee.Collection.Gui.ColumnCellDateTime
+    {
+        public ColumnCellPublished (string property, bool expand) : base (property, expand)
+        {
+            Format = Banshee.Collection.Gui.DateTimeFormat.ShortDate;
+            SetMinMaxStrings (new DateTime (2007, 12, 30));
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellUnheard.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellUnheard.cs
new file mode 100644
index 0000000..b1ba376
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/ColumnCellUnheard.cs
@@ -0,0 +1,57 @@
+//
+// ColumnCellUnheard.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+using Mono.Unix;
+
+using Hyena.Data.Gui;
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class ColumnCellUnheard : ColumnCellText
+    {
+        public ColumnCellUnheard () : base (null, true)
+        {
+        }
+
+        protected override string GetText (object obj)
+        {
+            OldNewFilter val = (OldNewFilter) obj;
+
+            switch (val) {
+                case OldNewFilter.New:  return Catalog.GetString ("New Episodes");
+                case OldNewFilter.Both: return Catalog.GetString ("All");
+                case OldNewFilter.Old:  return Catalog.GetString ("Reruns");
+            }
+
+            return String.Empty;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/Dialogs/ChannelPropertiesDialog.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/Dialogs/ChannelPropertiesDialog.cs
new file mode 100644
index 0000000..df4048a
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/Dialogs/ChannelPropertiesDialog.cs
@@ -0,0 +1,267 @@
+//
+// PodcastFeedPropertiesDialog.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2007-09 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// People have been asking for the ability to select a specific directory for
+// podcasts for almost four years.  I've been hesitant because it raises a number
+// of issues related to migrating existing tracks (notice that iTunes doesn't even
+// allow this.)  Right now you can select a directory for FUTURE downloads while
+// previously downloaded files remain in the previous directory.
+
+// I'm putting this in to satisfy certain people.  Honestly I don't plan to include
+// it when it comes time for a larger release (too many people are going to bitch
+// about it not moving existing existing files.  I may implement that at some point
+// just not now.)
+
+#undef EDIT_DIR_TEST
+
+using System;
+
+using Mono.Unix;
+
+using Gtk;
+using Pango;
+
+using Banshee.Base;
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    internal class ChannelPropertiesDialog : Dialog
+    {
+        private PaasChannel channel;
+#if EDIT_DIR_TEST
+        private FileChooserButton chooser;
+#endif
+        private DownloadPreferenceComboBox download_preference_combo;
+
+        public ChannelPropertiesDialog (PaasChannel channel)
+        {
+            this.channel = channel;
+
+            Title = channel.Name;
+
+            BuildWindow ();
+        }
+
+        private void BuildWindow ()
+        {
+            BorderWidth = 6;
+            VBox.Spacing = 12;
+            HasSeparator = false;
+
+            HBox box = new HBox ();
+            box.BorderWidth = 6;
+            box.Spacing = 12;
+
+            Button save_button = new Button ("gtk-save");
+            save_button.CanDefault = true;
+            save_button.Show();
+
+            // For later additions to the dialog.  (I.E. Feed art)
+            HBox content_box = new HBox ();
+            content_box.Spacing = 12;
+
+            Table table = new Table (2, 4, false);
+            table.RowSpacing = 6;
+            table.ColumnSpacing = 12;
+
+            Label description_label = new Label (Catalog.GetString ("Description:"));
+            description_label.SetAlignment (0f, 0f);
+            description_label.Justify = Justification.Left;
+
+            Label last_updated_label = new Label (Catalog.GetString ("Last updated:"));
+            last_updated_label.SetAlignment (0f, 0f);
+            last_updated_label.Justify = Justification.Left;
+
+            Label name_label = new Label (Catalog.GetString ("Name:"));
+            name_label.SetAlignment (0f, 0f);
+            name_label.Justify = Justification.Left;
+
+            Label name_label_ = new Label ();
+            name_label_.SetAlignment (0f, 0f);
+            name_label_.Text = channel.Name;
+
+            Label channel_url_label = new Label (Catalog.GetString ("URL:"));
+            channel_url_label.SetAlignment (0f, 0f);
+            channel_url_label.Justify = Justification.Left;
+
+            Label new_episode_option_label = new Label (Catalog.GetString ("When channel is updated:"));
+            new_episode_option_label.SetAlignment (0f, 0.5f);
+            new_episode_option_label.Justify = Justification.Left;
+
+#if EDIT_DIR_TEST
+            Label enclosure_folder_label = new Label (Catalog.GetString ("Folder:"));
+            enclosure_folder_label.SetAlignment (0f, 0.5f);
+            enclosure_folder_label.Justify = Justification.Left;
+#endif
+
+            Label last_updated_text = new Label (channel.LastDownloadTime.ToString ("f"));
+            last_updated_text.Justify = Justification.Left;
+            last_updated_text.SetAlignment (0f, 0f);
+
+            Label channel_url_text = new Label (channel.Url.ToString ());
+            channel_url_text.Wrap = false;
+            channel_url_text.Selectable = true;
+            channel_url_text.SetAlignment (0f, 0f);
+            channel_url_text.Justify = Justification.Left;
+            channel_url_text.Ellipsize = Pango.EllipsizeMode.End;
+
+            string description_string = String.IsNullOrEmpty (channel.Description) ?
+                                        Catalog.GetString ("No description available") :
+                                        channel.Description;
+
+            Label descrition_text = new Label (description_string);
+            descrition_text.Justify = Justification.Left;
+            descrition_text.SetAlignment (0f, 0f);
+            descrition_text.Wrap = true;
+            descrition_text.Selectable = true;
+
+            Viewport description_viewport = new Viewport ();
+            description_viewport.SetSizeRequest (-1, 150);
+            description_viewport.ShadowType = ShadowType.None;
+
+            ScrolledWindow description_scroller = new ScrolledWindow () {
+                HscrollbarPolicy = PolicyType.Never,
+                VscrollbarPolicy = PolicyType.Automatic
+            };
+
+            description_viewport.Add (descrition_text);
+            description_scroller.Add (description_viewport);
+
+#if EDIT_DIR_TEST
+            chooser = new FileChooserButton (Catalog.GetString ("File..."), FileChooserAction.SelectFolder);
+            chooser.SetCurrentFolder (channel.LocalEnclosurePath);
+#endif
+            download_preference_combo = new DownloadPreferenceComboBox (channel.DownloadPreference);
+
+            // First column
+            uint i = 0;
+            table.Attach (
+                name_label, 0, 1, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+
+            table.Attach (
+                channel_url_label, 0, 1, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+
+            table.Attach (
+                last_updated_label, 0, 1, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+
+#if EDIT_DIR_TEST
+            table.Attach (
+                enclosure_folder_label, 0, 1, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+#endif
+            table.Attach (
+                new_episode_option_label, 0, 1, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+
+            table.Attach (
+                description_label, 0, 1, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+
+            // Second column
+            i = 0;
+            table.Attach (
+                name_label_, 1, 2, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+
+            table.Attach (
+                channel_url_text, 1, 2, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+
+            table.Attach (
+                last_updated_text, 1, 2, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+
+#if EDIT_DIR_TEST
+            table.Attach (chooser, 1, 2, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+#endif
+            table.Attach (
+                download_preference_combo, 1, 2, i, ++i,
+                AttachOptions.Fill, AttachOptions.Fill, 0, 0
+            );
+
+            table.Attach (description_scroller, 1, 2, i, ++i,
+                AttachOptions.Expand | AttachOptions.Fill,
+                AttachOptions.Expand | AttachOptions.Fill, 0, 0
+            );
+
+            content_box.PackStart (table, true, true, 0);
+            box.PackStart (content_box, true, true, 0);
+
+            Button cancel_button = new Button("gtk-cancel");
+            cancel_button.CanDefault = true;
+            cancel_button.Show ();
+
+            AddActionWidget (cancel_button, ResponseType.Cancel);
+            AddActionWidget (save_button, ResponseType.Ok);
+
+            DefaultResponse = Gtk.ResponseType.Cancel;
+            ActionArea.Layout = Gtk.ButtonBoxStyle.End;
+
+            box.ShowAll ();
+            VBox.Add (box);
+
+            Response += OnResponse;
+        }
+
+        private void OnResponse (object sender, ResponseArgs args)
+        {
+            if (args.ResponseId == Gtk.ResponseType.Ok) {
+
+#if EDIT_DIR_TEST
+                if (channel.DownloadPreference != download_preference_combo.ActiveDownloadPreference ||
+                    channel.LocalEnclosurePath != chooser.CurrentFolder) {
+                    channel.LocalEnclosurePath = chooser.CurrentFolder;
+                    channel.DownloadPreference = download_preference_combo.ActiveDownloadPreference;
+                    channel.Save ();
+                }
+#else
+                if (channel.DownloadPreference != download_preference_combo.ActiveDownloadPreference) {
+                    channel.DownloadPreference = download_preference_combo.ActiveDownloadPreference;
+                    channel.Save ();
+                }
+#endif
+            }
+
+            (sender as Dialog).Response -= OnResponse;
+            (sender as Dialog).Destroy ();
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/Dialogs/SubscribeDialog.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/Dialogs/SubscribeDialog.cs
new file mode 100644
index 0000000..c31ebf1
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/Dialogs/SubscribeDialog.cs
@@ -0,0 +1,186 @@
+//
+// SubscribeDialog.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Gtk;
+using Mono.Unix;
+
+using Hyena.Widgets;
+
+using Banshee.Gui;
+using Banshee.Base;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    internal class SubscribeDialog : Dialog
+    {
+        private Entry url_entry;
+        private Gtk.AccelGroup accel_group;
+        private DownloadPreferenceComboBox download_pref_combo;
+
+        public string Url {
+            get { return url_entry.Text; }
+            set { url_entry.Text = value; }
+        }
+
+        public DownloadPreference DownloadPreference
+        {
+            get { return download_pref_combo.ActiveDownloadPreference; }
+        }
+
+        public SubscribeDialog () : base (Catalog.GetString("Subscribe"), null, DialogFlags.Modal | DialogFlags.NoSeparator)
+        {
+            accel_group = new Gtk.AccelGroup();
+            AddAccelGroup (accel_group);
+            BuildWindow ();
+        }
+
+        private void BuildWindow ()
+        {
+            DefaultWidth = 475;
+
+            BorderWidth = 6;
+            VBox.Spacing = 12;
+            ActionArea.Layout = Gtk.ButtonBoxStyle.End;
+
+            HBox box = new HBox();
+            box.BorderWidth = 6;
+            box.Spacing = 12;
+
+            Image image = new Image (IconThemeUtils.LoadIcon (48, "podcast"));
+
+            image.Yalign = 0.0f;
+
+            box.PackStart(image, false, true, 0);
+
+            VBox contentBox = new VBox();
+            contentBox.Spacing = 12;
+
+            Label header = new Label();
+            header.Markup = String.Format (
+                "<big><b>{0}</b></big>",
+                GLib.Markup.EscapeText (Catalog.GetString ("Subscribe to New Podcast"))
+            );
+
+            header.Justify = Justification.Left;
+            header.SetAlignment (0.0f, 0.0f);
+
+            WrapLabel message = new WrapLabel ();
+            message.Markup = Catalog.GetString (
+                "Please enter the URL of the podcast to which you would like to subscribe."
+            );
+
+            message.Wrap = true;
+
+            VBox sync_vbox = new VBox ();
+
+            VBox expander_children = new VBox();
+            //expander_children.BorderWidth = 6;
+            expander_children.Spacing = 6;
+
+            Label sync_text = new Label (
+                Catalog.GetString ("When new episodes are available:  ")
+            );
+
+            sync_text.SetAlignment (0.0f, 0.0f);
+            sync_text.Justify = Justification.Left;
+
+            download_pref_combo = new DownloadPreferenceComboBox ();
+
+            expander_children.PackStart (sync_text, true, true, 0);
+            expander_children.PackStart (download_pref_combo, true, true, 0);
+
+            sync_vbox.Add (expander_children);
+
+            url_entry = new Entry ();
+            url_entry.ActivatesDefault = true;
+
+            // If the user has copied some text to the clipboard that starts with http, set
+            // our url entry to it and select it
+            Clipboard clipboard = Clipboard.Get (Gdk.Atom.Intern ("CLIPBOARD", false));
+            if (clipboard != null) {
+                string pasted = clipboard.WaitForText ();
+                if (!String.IsNullOrEmpty (pasted)) {
+                    if (pasted.StartsWith ("http")) {
+                        url_entry.Text = pasted.Trim ();
+                        url_entry.SelectRegion (0, url_entry.Text.Length);
+                    }
+                }
+            }
+
+            Table table = new Table (1, 2, false);
+            table.RowSpacing = 6;
+            table.ColumnSpacing = 12;
+
+            table.Attach (
+                new Label (Catalog.GetString ("URL:")), 0, 1, 0, 1,
+                AttachOptions.Shrink, AttachOptions.Shrink, 0, 0
+            );
+
+            table.Attach (
+                url_entry, 1, 2, 0, 1,
+                AttachOptions.Expand | AttachOptions.Fill,
+                AttachOptions.Shrink, 0, 0
+            );
+
+            table.Attach (
+                sync_vbox, 0, 2, 1, 2,
+                AttachOptions.Expand | AttachOptions.Fill,
+                AttachOptions.Shrink, 0, 0
+            );
+
+            contentBox.PackStart (header, true, true, 0);
+            contentBox.PackStart (message, true, true, 0);
+
+            contentBox.PackStart (table, true, true, 0);
+
+            box.PackStart (contentBox, true, true, 0);
+
+            AddButton (Gtk.Stock.Cancel, Gtk.ResponseType.Cancel, true);
+            AddButton (Catalog.GetString ("Subscribe"), ResponseType.Ok, true);
+
+            box.ShowAll ();
+            VBox.Add (box);
+        }
+
+        private void AddButton (string stock_id, Gtk.ResponseType response, bool is_default)
+        {
+            Gtk.Button button = new Gtk.Button (stock_id);
+            button.CanDefault = true;
+            button.Show ();
+
+            AddActionWidget (button, response);
+
+            if (is_default) {
+                DefaultResponse = response;
+                button.AddAccelerator ( "activate", accel_group, (uint) Gdk.Key.Escape, 0, Gtk.AccelFlags.Visible);
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/DownloadPreferenceComboBox.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/DownloadPreferenceComboBox.cs
new file mode 100644
index 0000000..fe9b146
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/DownloadPreferenceComboBox.cs
@@ -0,0 +1,62 @@
+//
+// DownloadPreferenceComboBox.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using Gtk;
+using System;
+using Mono.Unix;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class DownloadPreferenceComboBox : Gtk.ComboBox
+    {
+        private static readonly string [] combo_text_entries = {
+            Catalog.GetString ("Download all episodes"),
+            Catalog.GetString ("Download the most recent episode"),
+            Catalog.GetString ("Let me decide which episodes to download")
+        };
+
+        public DownloadPreference ActiveDownloadPreference
+        {
+            get { return (DownloadPreference) Active; }
+        }
+
+        public DownloadPreferenceComboBox (DownloadPreference download_pref) : base (combo_text_entries)
+        {
+            if ((int) download_pref >= (int) DownloadPreference.All &&
+                (int) download_pref <= (int) DownloadPreference.None) {
+                Active = (int) download_pref;
+            } else {
+                Active = (int) DownloadPreference.One;
+            }
+        }
+
+        public DownloadPreferenceComboBox (): this (DownloadPreference.One)
+        {
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/DownloadStatusFilterView.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/DownloadStatusFilterView.cs
new file mode 100644
index 0000000..689c779
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/DownloadStatusFilterView.cs
@@ -0,0 +1,49 @@
+//
+// DownloadStatusFilterView.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using Hyena.Data.Gui;
+using Hyena.Collections;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+using Banshee.Collection.Gui;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class DownloadStatusFilterView : TrackFilterListView<DownloadedStatusFilter>
+    {
+        public DownloadStatusFilterView () : base ()
+        {
+            ColumnCellDownloadStatus renderer = new ColumnCellDownloadStatus ();
+            column_controller.Add (new Column ("Download Status Filter", renderer, 1.0));
+            ColumnController = column_controller;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/IColumnCellDataHelper.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/IColumnCellDataHelper.cs
new file mode 100644
index 0000000..3e9f2bf
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/IColumnCellDataHelper.cs
@@ -0,0 +1,39 @@
+//
+// IColumnCellDataHelper.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Hyena.Data.Gui;
+
+// I.  Hate.  This.
+namespace Banshee.Paas.Gui
+{
+    public delegate object ColumnCellDataHelper (ColumnCell cell, object state);
+
+    public interface IColumnCellDataHelper
+    {
+        ColumnCellDataHelper DataHelper { get; set; }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasActions.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasActions.cs
new file mode 100644
index 0000000..ef1a522
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasActions.cs
@@ -0,0 +1,793 @@
+//
+// PaasActions.cs
+//
+// Author:
+//    Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Gtk;
+
+using Migo2.Async;
+using Hyena.Collections;
+
+using Banshee.Gui;
+using Banshee.Widgets;
+
+using Banshee.Sources;
+using Banshee.ServiceStack;
+
+using Banshee.Collection;
+using Banshee.Collection.Database;
+
+using Banshee.Paas.Data;
+using Banshee.Paas.Aether;
+
+namespace Banshee.Paas.Gui
+{
+    enum SelectionInfo {
+        None,
+        One,
+        Multiple
+    }
+
+    [Flags]
+    enum SelectionOldNewInfo {
+        Zero    = 0x00,
+        ShowNew = 0x01,
+        ShowOld = 0x02
+    }
+
+    public class PaasActions : BansheeActionGroup
+    {
+        private uint actions_id;
+        private PaasService service;
+
+        private DatabaseSource last_source;
+
+        public PaasActions (PaasService service) : base (ServiceManager.Get<InterfaceActionService> (), "Paas")
+        {
+            this.service = service;
+
+            AddImportant (
+                new ActionEntry (
+                    "PaasUpdateAllAction", Stock.Refresh,
+                     Catalog.GetString ("Update Channels"), "<control><shift>U",
+                     null, OnPaasUpdateHandler
+                ),
+                new ActionEntry (
+                    "PaasSubscribeAction", Stock.Add,
+                     Catalog.GetString ("Subscribe to Channel"), null,
+                     null, OnPaasSubscribeHandler
+                )
+            );
+
+            Add (new ActionEntry [] {
+                new ActionEntry (
+                    "PaasImportOpmlAction", null,
+                     Catalog.GetString ("Import Channels from OPML"), null,
+                     null, OnPaasImportOpmlHandler
+                ),
+                new ActionEntry (
+                    "PaasExportAllOpmlAction", null,
+                     Catalog.GetString ("Export Channels as OPML"), null,
+                     null, OnPaasExportAllOpmlHandler
+                ),
+                new ActionEntry (
+                    "PaasExportOpmlAction", null,
+                     Catalog.GetString ("Export Selected as OPML"), null,
+                     null, OnPaasExportOpmlHandler
+                ),
+//                new ActionEntry (
+//                    "PaasMiroGuideSubscribeAction", null,
+//                     Catalog.GetString ("Export Selected to Miro Guide"), null,
+//                     null, OnPaasMiroguideSubscribeHandler
+//                ),
+//                new ActionEntry (
+//                    "PaasMiroGuideGetSubscriptionsAction", null,
+//                     Catalog.GetString ("Import Channels from Miro Guide"), null,
+//                     null, OnPaasGetMiroGuideSubscriptionsHandler
+//                ),
+                new ActionEntry (
+                    "PaasItemDownloadAction", Stock.SaveAs,
+                     Catalog.GetString ("Download"), null,
+                     null, OnPaasItemDownloadHandler
+                ),
+                new ActionEntry (
+                    "PaasItemCancelAction", Stock.Cancel,
+                     Catalog.GetString ("Cancel"), null,
+                     null, OnPaasItemCancelHandler
+                ),
+                new ActionEntry (
+                    "PaasItemPauseAction", Stock.MediaPause,
+                     Catalog.GetString ("Pause"), null,
+                     null, OnPaasItemPauseHandler
+                ),
+                new ActionEntry (
+                    "PaasItemResumeAction", Stock.Redo,
+                     Catalog.GetString ("Resume"), null,
+                     Catalog.GetString ("Resume"), OnPaasItemResumeHandler
+                ),
+                new ActionEntry (
+                    "PaasItemMarkNewAction", null,
+                     Catalog.GetString ("Mark as New"), null,
+                     null, OnPaasItemMarkedNewHandler
+                ),
+                new ActionEntry (
+                    "PaasItemMarkOldAction", null,
+                     Catalog.GetString ("Mark as Old"), null,
+                     null, OnPaasItemMarkedOldHandler
+                ),
+                new ActionEntry (
+                    "PaasItemRemoveAction", Stock.Remove,
+                     Catalog.GetString ("Remove From Library"), null,
+                     null, OnPaasItemRemovedHandler
+                ),
+                new ActionEntry (
+                    "PaasItemDeleteAction", null,
+                     Catalog.GetString ("Delete From Drive"), null,
+                     null, OnPaasItemDeletedHandler
+                ),
+                new ActionEntry (
+                    "PaasItemLinkAction", Stock.JumpTo,
+                     Catalog.GetString ("Visit Homepage"), null,
+                     null, OnPaasItemHomepageHandler
+                ),
+                new ActionEntry (
+                    "PaasChannelPopupAction", null, null, null, null, OnChannelPopup
+                ),
+                new ActionEntry (
+                    "PaasChannelUpdateAction", Stock.Refresh,
+                     Catalog.GetString ("Update"), null,
+                     null, OnPaasChannelUpdateHandler
+                ),
+                new ActionEntry (
+                    "PaasChannelDeleteAction", Stock.Delete,
+                     Catalog.GetString ("Unsubscribe and Delete"), null,
+                     null, OnPaasChannelDeleteHandler
+                ),
+                new ActionEntry (
+                    "PaasChannelHomepageAction", Stock.JumpTo,
+                     Catalog.GetString ("Visit Homepage"), null,
+                     null, OnPaasChannelHomepageHandler
+                ),
+                new ActionEntry (
+                    "PaasDownloadAllAction", Stock.SaveAs,
+                     Catalog.GetString ("Download All Episodes"), null,
+                     null, OnPaasDownloadAllHandler
+                ),
+                new ActionEntry (
+                    "PaasChannelDownloadAllAction", Stock.SaveAs,
+                     Catalog.GetString ("Download All Episodes"), null,
+                     null, OnPaasChannelDownloadAllHandler
+                ), new ActionEntry (
+                    "PaasChannelPropertiesAction", Stock.Preferences,
+                     Catalog.GetString ("Properties"), null,
+                     null, OnPaasChannelPropertiesHandler
+                )
+            });
+
+            actions_id = Actions.UIManager.AddUiFromResource ("GlobalUI.xml");
+            Actions.AddActionGroup (this);
+
+            ServiceManager.SourceManager.ActiveSourceChanged += HandleActiveSourceChanged;
+        }
+
+        public override void Dispose ()
+        {
+            Actions.UIManager.RemoveUi (actions_id);
+            Actions.RemoveActionGroup (this);
+            base.Dispose ();
+        }
+
+        private void HandleActiveSourceChanged (SourceEventArgs args)
+        {
+            last_source = args.Source as DatabaseSource;
+        }
+
+        private DatabaseSource ActiveDbSource {
+            get { return last_source; }
+        }
+
+        private bool IsPaasSource {
+            get {
+                return ActiveDbSource != null && (ActiveDbSource is PaasSource || ActiveDbSource.Parent is PaasSource);
+            }
+        }
+
+        public PaasChannelModel ActiveChannelModel {
+            get {
+                if (ActiveDbSource == null) {
+                    return null;
+                } else if (ActiveDbSource is PaasSource) {
+                    return (ActiveDbSource as PaasSource).ChannelModel;
+                } else {
+                    PaasChannelModel model = null;
+
+                    foreach (IFilterListModel filter in ActiveDbSource.AvailableFilters) {
+                        model = filter as PaasChannelModel;
+
+                        if (model != null) {
+                            break;
+                        }
+                    }
+
+                    return model;
+                }
+            }
+        }
+
+        private bool GetItemDownloadSelectionStatus (IEnumerable<PaasTrackInfo> items)
+        {
+            return GetItemDownloadSelectionStatus (items.Where (i => !i.IsDownloaded), TaskState.None) != SelectionInfo.None;
+        }
+
+        private SelectionInfo GetItemDownloadSelectionStatus (IEnumerable<PaasTrackInfo> items, TaskState state)
+        {
+            int cnt = -1;
+
+            foreach (PaasTrackInfo ti in items) {
+                if (CheckStatus (ti.Item, state)) {
+                    if (++cnt == 1) {
+                        break;
+                    }
+                }
+            }
+
+            switch (cnt) {
+            case 0:
+                return SelectionInfo.One;
+            case 1:
+                return SelectionInfo.Multiple;
+            default:
+                return SelectionInfo.None;
+            }
+        }
+
+        private SelectionOldNewInfo GetItemOldNewSelectionStatus (IEnumerable<PaasTrackInfo> items)
+        {
+            // C# needs multiple returns
+            bool show_new = false, show_old = false;
+            SelectionOldNewInfo info = SelectionOldNewInfo.Zero;
+
+            foreach (PaasItem i in items.Select (ti => ti.Item)) {
+                if (!i.IsDownloaded) {
+                    continue;
+                }
+
+                if (!show_old && i.IsNew) {
+                    show_old = true;
+                    info |= SelectionOldNewInfo.ShowOld;
+                } else if (!show_new && !i.IsNew) {
+                    show_new = true;
+                    info |= SelectionOldNewInfo.ShowNew;
+                }
+
+                if (show_new && show_old) {
+                    break;
+                }
+            }
+
+            return info;
+        }
+
+        private bool CheckStatus (PaasItem item, TaskState flags)
+        {
+            return (service.DownloadManager.CheckActiveDownloadStatus (item.DbId) & flags) != TaskState.Zero;
+        }
+
+        public void UpdateItemActions ()
+        {
+            UpdateItemActions (GetSelectedItems ());
+        }
+
+        public void UpdateItemActions (IEnumerable<PaasTrackInfo> items)
+        {
+            if (!IsPaasSource) {
+                return;
+            }
+
+            bool show_download = GetItemDownloadSelectionStatus (items);
+            bool show_cancel   = GetItemDownloadSelectionStatus (items, TaskState.CanCancel) != SelectionInfo.None;
+            bool show_resume   = GetItemDownloadSelectionStatus (items, TaskState.Paused)    != SelectionInfo.None;
+            bool show_pause    = GetItemDownloadSelectionStatus (items, TaskState.CanPause)  != SelectionInfo.None;
+
+            SelectionOldNewInfo selection = GetItemOldNewSelectionStatus (items);
+            bool show_mark_new = ((selection & SelectionOldNewInfo.ShowNew) != SelectionOldNewInfo.Zero);
+            bool show_mark_old = ((selection & SelectionOldNewInfo.ShowOld) != SelectionOldNewInfo.Zero);
+
+            UpdateAction ("PaasItemDownloadAction", show_download);
+            UpdateAction ("PaasItemCancelAction", show_cancel);
+            UpdateAction ("PaasItemPauseAction", show_pause);
+            UpdateAction ("PaasItemResumeAction", show_resume);
+
+            UpdateAction ("PaasItemMarkNewAction", show_mark_new);
+            UpdateAction ("PaasItemMarkOldAction", show_mark_old);
+
+            UpdateAction ("PaasItemLinkAction", (ActiveDbSource.TrackModel.Selection.Count == 1));
+        }
+
+        public void UpdateChannelActions ()
+        {
+            if (!IsPaasSource) {
+                return;
+            }
+
+            UpdateActions (
+                true,
+                (ActiveChannelModel.Selection.Count == 1 &&
+                !ActiveChannelModel.Selection.AllSelected),
+                "PaasChannelHomepageAction",
+                "PaasChannelPropertiesAction"
+            );
+        }
+
+        private IEnumerable<PaasChannel> GetSelectedChannels ()
+        {
+            return new List<PaasChannel> (ActiveChannelModel.SelectedItems);
+        }
+
+        private IEnumerable<PaasTrackInfo> GetSelectedItems ()
+        {
+            return new List<PaasTrackInfo> (
+                PaasTrackInfo.From (ActiveDbSource.TrackModel.SelectedItems)
+            );
+        }
+
+        private void RunSubscribeDialog ()
+        {
+            SubscribeDialog dialog = new SubscribeDialog ();
+            ResponseType response = (ResponseType) dialog.Run ();
+            dialog.Destroy ();
+
+            if (response == ResponseType.Ok) {
+                if (String.IsNullOrEmpty (dialog.Url)) {
+                    return;
+                }
+
+                string url = dialog.Url.Trim ().Trim ('/');
+                DownloadPreference download_pref = dialog.DownloadPreference;;
+
+                try {
+                    service.SyndicationClient.SubscribeToChannel (url, download_pref);
+                } catch (Exception e) {
+                    Hyena.Log.Exception (e);
+
+                    HigMessageDialog.RunHigMessageDialog (
+                        null,
+                        DialogFlags.Modal,
+                        MessageType.Warning,
+                        ButtonsType.Ok,
+                        Catalog.GetString ("Invalid URL"),
+                        Catalog.GetString ("Podcast URL is invalid.")
+                    );
+                }
+            }
+        }
+
+        private void MarkItems (IEnumerable<PaasTrackInfo> items, bool _new)
+        {
+            PaasSource s = ActiveDbSource as PaasSource;
+
+            if (s == null) {
+                return;
+            }
+
+            RangeCollection rc = new RangeCollection ();
+
+            foreach (var i in items.Select (i => i.Item)) {
+                if (!i.IsDownloaded) {
+                    continue;
+                }
+
+                if (_new && !i.IsNew) {
+                    i.IsNew = true;
+                    rc.Add ((int)i.DbId);
+                } else if (i.IsNew) {
+                    i.IsNew = false;
+                    rc.Add ((int)i.DbId);
+                }
+            }
+
+            foreach (var range in rc.Ranges) {
+                ServiceManager.DbConnection.Execute (
+                    String.Format ("UPDATE PaasItems SET IsNew = ? WHERE ID >= ? AND ID <= ?"),
+                    (_new ? 1 : 0), range.Start, range.End
+                );
+            }
+
+            s.Reload ();
+        }
+
+        private void RunConfirmDeleteDialog (bool channel, int selCount, out bool delete, out bool deleteFiles)
+        {
+
+            delete = false;
+            deleteFiles = false;
+            string header = null;
+            int plural = (channel | (selCount > 1)) ? 2 : 1;
+            int channel_count = 0;
+
+            if (channel) {
+                channel_count = GetSelectedChannels ().Count ();
+                header = Catalog.GetPluralString ("Delete Channel?", "Delete Channels?", channel_count);
+            } else {
+                header = Catalog.GetPluralString ("Delete Item?", "Delete Items?", selCount);
+            }
+
+            HigMessageDialog md = null;
+
+            if (selCount > 0) {
+                md = new HigMessageDialog (
+                    ServiceManager.Get<GtkElementsService> ("GtkElementsService").PrimaryWindow,
+                    DialogFlags.DestroyWithParent,
+                    MessageType.Question,
+                    ButtonsType.None, header,
+                    Catalog.GetPluralString (
+                        "Would you like to delete the associated file?",
+                        "Would you like to delete the associated files?", plural
+                    )
+                );
+
+                md.AddButton (Stock.Cancel, ResponseType.Cancel, true);
+                md.AddButton (Catalog.GetPluralString ("Keep File", "Keep Files", plural), ResponseType.No, false);
+                md.AddButton (Stock.Delete, ResponseType.Yes, false);
+            } else {
+                md = new HigMessageDialog (
+                    ServiceManager.Get<GtkElementsService> ("GtkElementsService").PrimaryWindow,
+                    DialogFlags.DestroyWithParent,
+                    MessageType.Question,
+                    ButtonsType.None, header,
+                    Catalog.GetPluralString (
+                        "Would you like to delete the selected channel?",
+                        "Would you like to delete the selected channels?", channel_count
+                    )
+                );
+
+                md.AddButton (Stock.Cancel, ResponseType.Cancel, true);
+                md.AddButton (Stock.Delete, ResponseType.Yes, false);
+            }
+
+            try {
+                switch ((ResponseType)md.Run ()) {
+                case ResponseType.Yes:
+                    deleteFiles = true;
+                    goto case ResponseType.No;
+                case ResponseType.No:
+                    delete = true;
+                    break;
+                }
+            } finally {
+                md.Destroy ();
+            }
+        }
+
+        private ResponseType RunConfirmRemoveDeleteDialog (bool delete, int itemCount)
+        {
+            ResponseType response;
+
+            HigMessageDialog md = new HigMessageDialog (
+                ServiceManager.Get<GtkElementsService> ("GtkElementsService").PrimaryWindow,
+                DialogFlags.DestroyWithParent,
+                MessageType.Question,
+                ButtonsType.None,
+                String.Format (
+                    "{0} {1}",
+                    delete ? Catalog.GetString ("Delete") : Catalog.GetString ("Remove"),
+                    Catalog.GetPluralString ("Item?", "Items?", itemCount)
+                ),
+                String.Format (
+                    Catalog.GetString ("Are you sure that you want to {0} the selected {1}?"),
+                    delete ? Catalog.GetString ("delete") : Catalog.GetString ("remove"),
+                    Catalog.GetPluralString ("item", "items", itemCount)
+                )
+            );
+
+            md.AddButton (Stock.Cancel, ResponseType.Cancel, true);
+            md.AddButton ((delete ? Stock.Delete : Stock.Remove), ResponseType.Ok, false);
+
+            response = (ResponseType) md.Run ();
+            md.Destroy ();
+
+            return response;
+        }
+
+//        private void ExportToMiroGuide (IEnumerable<PaasChannel> channels)
+//        {
+//            service.MiroGuideClient.AddSubscriptionsAsync (channels.Select (c => c.Url));
+//        }
+
+        private void ExportToOpml (IEnumerable<PaasChannel> channels)
+        {
+            if (channels.Count () == 0) {
+                HigMessageDialog.RunHigMessageDialog (
+                    null, DialogFlags.Modal, MessageType.Warning, ButtonsType.Ok,
+                    Catalog.GetString ("Error Exporting Channels!"),
+                    Catalog.GetString ("No channels to export.")
+                );
+
+                return;
+            }
+
+            FileChooserDialog chooser = new FileChooserDialog (
+                Catalog.GetString ("Save As..."), null, FileChooserAction.Save,
+                new object[] {
+                    Stock.Cancel, ResponseType.Cancel,
+                    Stock.Save, ResponseType.Ok
+                }
+            );
+
+            chooser.Response += (o, ea) => {
+                if (ea.ResponseId == Gtk.ResponseType.Ok) {
+                    try {
+                        service.ExportChannelsToOpml (chooser.Filename, channels);
+                    } catch (Exception ex) {
+                       HigMessageDialog.RunHigMessageDialog (
+                            null, DialogFlags.Modal, MessageType.Warning, ButtonsType.Ok,
+                            Catalog.GetString ("Error Exporting Channels!"), ex.Message
+                        );
+                    }
+                }
+
+                (o as Dialog).Destroy ();
+            };
+
+            chooser.Run ();
+        }
+
+//        private void OnPaasMiroguideSubscribeHandler (object sender, EventArgs e)
+//        {
+//            ExportToMiroGuide (GetSelectedChannels ());
+//        }
+//
+//        private void OnPaasGetMiroGuideSubscriptionsHandler (object sender, EventArgs e)
+//        {
+//            service.MiroGuideClient.GetSubscriptionsAsync ();
+//        }
+
+        private void OnPaasExportOpmlHandler (object sender, EventArgs e)
+        {
+            ExportToOpml (GetSelectedChannels ());
+        }
+
+        private void OnPaasExportAllOpmlHandler (object sender, EventArgs e)
+        {
+            ExportToOpml (PaasChannel.Provider.FetchAll ());
+        }
+
+        private void OnPaasImportOpmlHandler (object sender, EventArgs e)
+        {
+            FileChooserDialog chooser = new FileChooserDialog (
+                Catalog.GetString ("Please Select a File to Import..."), null, FileChooserAction.Open,
+                new object[] {
+                    Stock.Cancel, ResponseType.Cancel,
+                    Catalog.GetString ("Import"), ResponseType.Ok
+                }
+            );
+
+            FileFilter filter = new FileFilter () {
+                Name = "OPML"
+            };
+
+            FileFilter unfiltered = new FileFilter () {
+                Name = Catalog.GetString ("All")
+            };
+
+            unfiltered.AddPattern ("*");
+            filter.AddPattern ("*.opml");
+            filter.AddPattern ("*.miro");
+
+            chooser.AddFilter (filter);
+            chooser.AddFilter (unfiltered);
+
+            chooser.Response += (o, ea) => {
+                if (ea.ResponseId == Gtk.ResponseType.Ok) {
+                    try {
+                        service.ImportOpml (chooser.Filename);
+                    } catch (Exception ex) {
+                       HigMessageDialog.RunHigMessageDialog (
+                            null, DialogFlags.Modal, MessageType.Warning, ButtonsType.Ok,
+                            Catalog.GetString ("Error Importing Channels!"), ex.Message
+                        );
+
+                        chooser.Run ();
+                    }
+                }
+
+                (o as Dialog).Destroy ();
+            };
+
+            chooser.Run ();
+        }
+
+        private void OnPaasSubscribeHandler (object sender, EventArgs e)
+        {
+            RunSubscribeDialog ();
+        }
+
+        private void OnPaasUpdateHandler (object sender, EventArgs e)
+        {
+            service.UpdateAsync ();
+        }
+        private void OnPaasItemDownloadHandler (object sender, EventArgs e)
+        {
+            var items = GetSelectedItems ();
+            service.QueueDownload (items.Select (ti => ti.Item).Where (i => !i.IsDownloaded));
+        }
+
+        private void OnPaasItemCancelHandler (object sender, EventArgs e)
+        {
+            var items = GetSelectedItems ();
+            service.DownloadManager.CancelDownload (
+                items.Select (t => t.Item).Where  (i => CheckStatus (i, TaskState.CanCancel))
+            );
+        }
+
+        private void OnPaasItemResumeHandler (object sender, EventArgs e)
+        {
+            var items = GetSelectedItems ();
+            service.DownloadManager.ResumeDownload (
+                items.Select (t => t.Item).Where  (i => CheckStatus (i, TaskState.Paused))
+            );
+        }
+
+        private void OnPaasItemPauseHandler (object sender, EventArgs e)
+        {
+            var items = GetSelectedItems ();
+            service.DownloadManager.PauseDownload (
+                items.Select (t => t.Item).Where  (i => CheckStatus (i, TaskState.CanPause))
+            );
+        }
+
+        private void OnPaasItemHomepageHandler (object sender, EventArgs e)
+        {
+            PaasItem item = PaasTrackInfo.From (ActiveDbSource.TrackModel.FocusedItem).Item;
+            if (item != null && !String.IsNullOrEmpty (item.Link)) {
+                Banshee.Web.Browser.Open (item.Link);
+            }
+        }
+
+        private void OnPaasItemDeletedHandler (object sender, EventArgs e)
+        {
+            var items = GetSelectedItems ();
+            if (RunConfirmRemoveDeleteDialog (true, items.Count ()) == ResponseType.Ok) {
+                service.SyndicationClient.RemoveItems (items.Select (t => t.Item), true);
+            }
+        }
+
+        private void OnPaasItemRemovedHandler (object sender, EventArgs e)
+        {
+            bool delete, delete_files;
+
+            var items = GetSelectedItems ().Select (t => t.Item);
+            int cnt = GetSelectedItems ().Select (t => t.Item).Where (i => i.IsDownloaded).Count ();
+
+            if (cnt > 0) {
+                RunConfirmDeleteDialog (false, cnt, out delete, out delete_files);
+
+                if (delete) {
+                    service.SyndicationClient.RemoveItems (items, delete_files);
+                }
+            } else {
+                if (RunConfirmRemoveDeleteDialog (false, items.Count ()) == ResponseType.Ok) {
+                    service.SyndicationClient.RemoveItems (items);
+                }
+            }
+        }
+
+        private void OnPaasItemMarkedNewHandler (object sender, EventArgs e)
+        {
+            var items = GetSelectedItems ();
+            MarkItems (items, true);
+        }
+
+        private void OnPaasItemMarkedOldHandler (object sender, EventArgs e)
+        {
+            var items = GetSelectedItems ();
+            MarkItems (items, false);
+        }
+
+        private void OnChannelPopup (object o, EventArgs e)
+        {
+            if (ActiveChannelModel.Selection.AllSelected) {
+                ShowContextMenu ("/PaasAllChannelsContextMenu");
+            } else {
+                ShowContextMenu ("/PaasChannelPopup");
+            }
+        }
+
+        private void OnPaasChannelUpdateHandler (object sender, EventArgs e)
+        {
+            var channels = GetSelectedChannels ();
+            service.SyndicationClient.QueueUpdate (channels);
+        }
+
+        private void OnPaasChannelDeleteHandler (object sender, EventArgs e)
+        {
+            int cnt = 0;
+            bool delete = true, delete_files = false;
+
+            var channels = GetSelectedChannels ();
+
+            foreach (var channel in channels) {
+                foreach (var item in channel.Items) {
+                    if (item.Active && item.IsDownloaded) {
+                        if (++cnt == 2) {
+                            break;
+                        }
+                    }
+                }
+
+                if (cnt > 0) {
+                    RunConfirmDeleteDialog (true, cnt, out delete, out delete_files);
+                    break;
+                }
+            }
+
+            if (cnt == 0) {
+                RunConfirmDeleteDialog (true, 0, out delete, out delete_files);
+            }
+
+            if (delete) {
+                service.DeleteChannels (channels, delete_files);
+            }
+        }
+
+        private void OnPaasChannelHomepageHandler (object sender, EventArgs e)
+        {
+            PaasChannel channel = ActiveChannelModel.FocusedItem;
+            if (channel != null && !String.IsNullOrEmpty (channel.Link)) {
+                Banshee.Web.Browser.Open (channel.Link);
+            }
+        }
+
+        private void OnPaasDownloadAllHandler (object sender, EventArgs e)
+        {
+            var channels = new List<PaasChannel> (PaasChannel.Provider.FetchAll ().OrderBy (c => c.Name));
+            foreach (var c in channels) {
+                service.QueueDownload (c.Items.Where (i => i.Active && !i.IsDownloaded));
+            }
+        }
+
+        private void OnPaasChannelDownloadAllHandler (object sender, EventArgs e)
+        {
+            var channels = GetSelectedChannels ();
+            foreach (var c in channels) {
+                service.QueueDownload (c.Items.Where (i => i.Active && !i.IsDownloaded));
+            }
+        }
+
+        private void OnPaasChannelPropertiesHandler (object sender, EventArgs e)
+        {
+            PaasChannel channel = ActiveChannelModel.FocusedItem;
+
+            if (channel != null) {
+                new ChannelPropertiesDialog (channel).Run ();
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasChannelView.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasChannelView.cs
new file mode 100644
index 0000000..ead8fce
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasChannelView.cs
@@ -0,0 +1,78 @@
+//
+// PaasChannelView.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Hyena.Data.Gui;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+using Banshee.Collection.Gui;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class PaasChannelView : TrackFilterListView<PaasChannel>
+    {
+        // Awful, dirty, filthy hack.
+        // I'm having a similar problem, probably just need to tinker with the event flags... Need to move on for now...
+        // http://lists.ximian.com/archives/public/gtk-sharp-list/2006-June/007247.html
+        public EventHandler<EventArgs> FuckedPopupMenu;
+
+        private ColumnCellChannel renderer;
+
+        public PaasChannelView ()
+        {
+            renderer = new ColumnCellChannel ();
+
+            column_controller.Add (new Column ("Channels", renderer, 1.0));
+
+            ColumnController  = column_controller;
+            RowHeightProvider = renderer.ComputeRowHeight;
+        }
+
+        public void SetChannelDataHelper (ColumnCellDataHelper dataHelper)
+        {
+            renderer.DataHelper = dataHelper;
+        }
+
+        protected override bool OnPopupMenu ()
+        {
+            EventHandler<EventArgs> handler = FuckedPopupMenu;
+
+            if (handler != null) {
+                handler (this, EventArgs.Empty);
+            }
+
+            ServiceManager.Get<InterfaceActionService> ().FindAction ("Paas.PaasChannelPopupAction").Activate ();
+
+            return true;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasColumnController.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasColumnController.cs
new file mode 100644
index 0000000..cfab0a7
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasColumnController.cs
@@ -0,0 +1,100 @@
+//
+// PaasColumnController.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Mono.Unix;
+
+using Hyena.Data;
+using Hyena.Data.Gui;
+using Banshee.Collection.Gui;
+
+namespace Banshee.Paas.Gui
+{
+    public class PaasColumnController : XmlColumnController
+    {
+        private static readonly string ColumnXml = String.Format (@"
+                <column-controller>
+                  <add-all-defaults/>
+                  <column modify-default=""IndicatorColumn"">
+                    <renderer type=""Banshee.Paas.Gui.ColumnCellPaasStatusIndicator"" />
+                  </column>
+                  <remove-default column=""TrackColumn"" />
+                  <remove-default column=""DiscColumn"" />
+                  <remove-default column=""ComposerColumn"" />
+                  <remove-default column=""ArtistColumn"" />
+                  <column modify-default=""AlbumColumn"">
+                    <title>{0}</title>
+                    <long-title>{0}</long-title>
+                  </column>
+                  <column>
+                      <visible>false</visible>
+                      <title>{4}</title>
+                      <renderer type=""Hyena.Data.Gui.ColumnCellText"" property=""ExternalObject.Description"" />
+                      <sort-key>Description</sort-key>
+                  </column>
+                  <column modify-default=""FileSizeColumn"">
+                      <visible>true</visible>
+                  </column>
+                  <!--
+                  <column>
+                      <visible>false</visible>
+                      <title>{2}</title>
+                      <renderer type=""Banshee.Podcasting.Gui.ColumnCellYesNo"" property=""ExternalObject.IsNew"" />
+                      <sort-key>IsNew</sort-key>
+                  </column>
+                  <column>
+                      <visible>false</visible>
+                      <title>{3}</title>
+                      <renderer type=""Banshee.Podcasting.Gui.ColumnCellYesNo"" property=""ExternalObject.IsDownloaded"" />
+                      <sort-key>IsDownloaded</sort-key>
+                  </column>
+                  -->
+
+                  <column>
+                      <visible>true</visible>
+                      <title>{1}</title>
+                      <renderer type=""Banshee.Paas.Gui.ColumnCellPublished"" property=""ExternalObject.PubDate"" />
+                      <sort-key>PubDate</sort-key>
+                  </column>
+                  <sort-column direction=""desc"">published_date</sort-column>
+                </column-controller>
+            ",
+            Catalog.GetString ("Channel"), Catalog.GetString ("Published"), Catalog.GetString ("New"),
+            Catalog.GetString ("Downloaded"), Catalog.GetString ("Description")
+        );
+
+        public PaasColumnController () : base (ColumnXml)
+        {
+        }
+
+        public void SetIndicatorColumnDataHelper (ColumnCellDataHelper dataHelper)
+        {
+            ColumnCellPaasStatusIndicator indicator = IndicatorColumn.GetCell (0) as ColumnCellPaasStatusIndicator;
+            indicator.DataHelper = dataHelper;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasItemPage.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasItemPage.cs
new file mode 100644
index 0000000..c2ff273
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasItemPage.cs
@@ -0,0 +1,127 @@
+//
+// PaasItemPage.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+
+using Mono.Unix;
+using Gtk;
+
+using Hyena.Widgets;
+
+using Banshee.Gui.TrackEditor;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class PaasItemPage : Gtk.ScrolledWindow, ITrackEditorPage
+    {
+        private VBox box;
+
+        private WrapLabel podcast       = new WrapLabel ();
+        private WrapLabel author        = new WrapLabel ();
+        private WrapLabel published     = new WrapLabel ();
+        private WrapLabel description   = new WrapLabel ();
+
+        public PaasItemPage ()
+        {
+            BorderWidth = 2;
+            ShadowType = ShadowType.None;
+            HscrollbarPolicy = PolicyType.Never;
+            VscrollbarPolicy = PolicyType.Automatic;
+
+            box = new VBox ();
+            box.BorderWidth = 6;
+            box.Spacing = 12;
+
+            box.PackStart (podcast,     false, false, 0);
+            box.PackStart (author,      false, false, 0);
+            box.PackStart (published,   false, false, 0);
+            box.PackStart (description, true, true, 0);
+
+            AddWithViewport (box);
+            ShowAll ();
+        }
+
+        public void Initialize (TrackEditorDialog dialog)
+        {
+        }
+
+        public void LoadTrack (EditorTrackInfo track)
+        {
+            BorderWidth = 2;
+
+            PaasTrackInfo info = PaasTrackInfo.From (track.SourceTrack);
+
+            if (info == null) {
+                Hide ();
+                return;
+            }
+            // HACK to keep the save TrackInfo dialog from appearing when closing the TED.
+            // Investigate / talk to Gabe / Aaron about this later.
+            track.ReleaseDate = info.PubDate;
+
+            podcast.Markup      = SetInfo (Catalog.GetString ("Podcast"), track.SourceTrack.AlbumTitle);
+            author.Markup       = SetInfo (Catalog.GetString ("Author"), track.SourceTrack.ArtistName);
+            published.Markup    = SetInfo (Catalog.GetString ("Published"), info.PubDate.ToLongDateString ());
+            description.Markup  = SetInfo (Catalog.GetString ("Description"), info.Description);
+
+            Show ();
+        }
+
+        private static string info_str = "<b>{0}</b>\n{1}";
+        private static string SetInfo (string title, string info)
+        {
+            return String.Format (info_str,
+                GLib.Markup.EscapeText (title),
+                GLib.Markup.EscapeText (info)
+            );
+        }
+
+        public int Order {
+            get { return 40; }
+        }
+
+        public string Title {
+            get { return Catalog.GetString ("Episode Details"); }
+        }
+
+        public PageType PageType {
+            get { return PageType.View; }
+        }
+
+        public Gtk.Widget TabWidget {
+            get { return null; }
+        }
+
+        public Gtk.Widget Widget {
+            get { return this; }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasItemView.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasItemView.cs
new file mode 100644
index 0000000..3dbe809
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasItemView.cs
@@ -0,0 +1,93 @@
+//
+// PaasItemView.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Gtk;
+
+using Hyena.Data.Gui;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+
+using Banshee.Collection;
+using Banshee.Collection.Gui;
+using Banshee.Collection.Database;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class PaasItemView : TrackListView
+    {
+        // Awful, dirty, filthy hack.
+        // I'm having a similar problem, probably just need to tinker with the event flags... Need to move on for now...
+        // http://lists.ximian.com/archives/public/gtk-sharp-list/2006-June/007247.html
+        public EventHandler<EventArgs> FuckedPopupMenu;
+
+        public PaasItemView ()
+        {
+        }
+
+        protected override bool OnPopupMenu ()
+        {
+            EventHandler<EventArgs> handler = FuckedPopupMenu;
+
+            if (handler != null) {
+                handler (this, EventArgs.Empty);
+            }
+
+            return base.OnPopupMenu ();
+        }
+
+        protected override void ColumnCellDataProvider (ColumnCell cell, object boundItem)
+        {
+            ColumnCellText text_cell = cell as ColumnCellText;
+
+            if (text_cell == null) {
+                return;
+            }
+
+            DatabaseTrackInfo track = boundItem as DatabaseTrackInfo;
+
+            if (track != null) {
+                PaasTrackInfo pti = PaasTrackInfo.From (track);
+
+                if (pti == null) {
+                    return;
+                }
+
+                if (track.IsPlaying || pti.IsDownloaded) {
+                    text_cell.Sensitive = true;
+                } else {
+                    text_cell.Sensitive = false;
+                }
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasSourceContents.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasSourceContents.cs
new file mode 100644
index 0000000..2139bf2
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasSourceContents.cs
@@ -0,0 +1,153 @@
+//
+// PaasSourceContents.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+#define SHOW_EXTRA_FILTERS
+
+using System;
+
+using Hyena.Data;
+using Hyena.Data.Gui;
+
+using Banshee.Sources;
+using Banshee.Sources.Gui;
+
+using Banshee.Collection;
+using Banshee.ServiceStack;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class PaasSourceContents : FilteredListSourceContents, ITrackModelSourceContents
+    {
+        private PaasItemView item_view;
+        private PaasChannelView channel_view;
+
+#if SHOW_EXTRA_FILTERS
+        private PaasUnheardFilterView unheard_view;
+        private DownloadStatusFilterView download_view;
+#endif
+
+        public PaasChannelView ChannelView
+        {
+            get { return channel_view; }
+        }
+
+        public PaasSourceContents () : base ("paas")
+        {
+        }
+
+        protected override void InitializeViews ()
+        {
+            SetupMainView   (item_view    = new PaasItemView ());
+            SetupFilterView (channel_view = new PaasChannelView ());
+#if SHOW_EXTRA_FILTERS
+            SetupFilterView (unheard_view = new PaasUnheardFilterView ());
+            SetupFilterView (download_view = new DownloadStatusFilterView ());
+#endif
+        }
+
+        protected override void ClearFilterSelections ()
+        {
+
+            if (channel_view.Model != null) {
+                channel_view.Selection.Clear ();
+#if SHOW_EXTRA_FILTERS
+                unheard_view.Selection.Clear ();
+                download_view.Selection.Clear ();
+#endif
+            }
+        }
+
+        protected override bool ActiveSourceCanHasBrowser {
+            get {
+                DatabaseSource db_src = ServiceManager.SourceManager.ActiveSource as DatabaseSource;
+                return db_src != null && db_src.ShowBrowser;
+            }
+        }
+
+        #region Implement ISourceContents
+
+        public override bool SetSource (ISource source)
+        {
+            DatabaseSource track_source = source as DatabaseSource;
+
+            if (track_source == null) {
+                return false;
+            }
+
+            this.source = source;
+
+            SetModel (item_view, track_source.TrackModel);
+
+            foreach (IListModel model in track_source.CurrentFilters) {
+                if (model is PaasChannelModel) {
+                    SetModel (channel_view, (model as IListModel<PaasChannel>));
+                }
+#if SHOW_EXTRA_FILTERS
+                else if (model is PaasUnheardFilterModel) {
+                    SetModel (unheard_view, (model as IListModel<OldNewFilter>));
+                } else if (model is DownloadStatusFilterModel) {
+                    SetModel (download_view, (model as IListModel<DownloadedStatusFilter>));
+                }
+#endif
+                else {
+                    Hyena.Log.DebugFormat ("PaasSourceContents got non-channel filter model: {0}", model);
+                }
+            }
+
+            item_view.HeaderVisible = true;
+
+            return true;
+        }
+
+        public override void ResetSource ()
+        {
+            source = null;
+
+            SetModel (item_view, null);
+            SetModel (channel_view, null);
+
+#if SHOW_EXTRA_FILTERS
+            SetModel (download_view, null);
+            SetModel (unheard_view, null);
+#endif
+            item_view.HeaderVisible = false;
+        }
+
+        #endregion
+
+        #region ITrackModelSourceContents implementation
+
+        public IListView<TrackInfo> TrackView {
+            get { return item_view; }
+        }
+
+        #endregion
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasUnheardFilterView.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasUnheardFilterView.cs
new file mode 100644
index 0000000..61bf10b
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Gui/PaasUnheardFilterView.cs
@@ -0,0 +1,49 @@
+//
+// PaasUnheardFilterView.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using Hyena.Data.Gui;
+using Hyena.Collections;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+using Banshee.Collection.Gui;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.Gui
+{
+    public class PaasUnheardFilterView : TrackFilterListView<OldNewFilter>
+    {
+        public PaasUnheardFilterView () : base ()
+        {
+            ColumnCellUnheard renderer = new ColumnCellUnheard ();
+            column_controller.Add (new Column ("Unheard Filter", renderer, 1.0));
+            ColumnController = column_controller;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ChannelInfoPreview.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ChannelInfoPreview.cs
new file mode 100644
index 0000000..eee8241
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ChannelInfoPreview.cs
@@ -0,0 +1,88 @@
+//
+// ChannelInfoPreview.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Cairo;
+
+using Hyena.Gui;
+using Banshee.Gui;
+
+using Banshee.ServiceStack;
+using Banshee.Collection.Gui;
+
+using Banshee.Paas;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    // This needs a good default state...
+    public class ChannelInfoPreview : ReflectionInfoWidget
+    {
+        private ArtworkManager artwork_manager;
+        private MiroGuideChannelInfo channel_info;
+
+        private static ImageSurface default_channel_image = new PixbufImageSurface (
+            IconThemeUtils.LoadIcon ("miroguide-default-channel", 256)
+        );
+
+        public MiroGuideChannelInfo ChannelInfo {
+            get { return channel_info; }
+            set {
+                if (value != channel_info) {
+                    channel_info = value;
+                    QueueDraw ();
+                }
+            }
+        }
+
+        public ChannelInfoPreview ()
+        {
+            artwork_manager = ServiceManager.Get<ArtworkManager> ();
+        }
+
+        protected override bool OnExposeEvent (Gdk.EventExpose evnt)
+        {
+            // Reset the text / image data on draw in case the image has changed since
+            // the ChannelInfo was set.  (e.g.  the image was just downloaded.)
+
+            // There is a flicker on draw, but it was here before I added this.
+
+            if (channel_info != null) {
+                /* Set renderer info... */
+                ImageSurface image = (artwork_manager == null) ? null
+                    : artwork_manager.LookupSurface (PaasService.ArtworkIdFor (channel_info.Name));
+
+                ReflectionImage = image ?? default_channel_image;
+            } else {
+                ReflectionImage = default_channel_image;
+                /* Clear renderer info, show default image / text */
+            }
+
+            return base.OnExposeEvent (evnt);
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ColumnCellChannel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ColumnCellChannel.cs
new file mode 100644
index 0000000..1a799c6
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ColumnCellChannel.cs
@@ -0,0 +1,166 @@
+//
+// ColumnCellChannel.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Gtk;
+using Cairo;
+
+using Mono.Unix;
+
+using Hyena.Gui;
+using Hyena.Gui.Theming;
+using Hyena.Data.Gui;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+using Banshee.Collection.Gui;
+
+using Banshee.Paas.Aether.MiroGuide;
+
+// This needs to be fixed!  Still has code to support two lines of text.
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class ColumnCellChannel : ColumnCell
+    {
+        private static int image_spacing = 4;
+        private static int image_size = 48;
+
+        // TODO replace this w/ new icon installation etc
+        private static ImageSurface default_cover_image = new PixbufImageSurface (
+            IconThemeUtils.LoadIcon (image_size, "miro")
+        );
+
+        private ArtworkManager artwork_manager;
+
+        public ColumnCellChannel () : base (null, true)
+        {
+            artwork_manager = ServiceManager.Get<ArtworkManager> ();
+        }
+
+        public override void Render (CellContext context, StateType state, double cellWidth, double cellHeight)
+        {
+            if (BoundObject == null) {
+                return;
+            }
+
+            if (!(BoundObject is MiroGuideChannelInfo)) {
+                throw new InvalidCastException("ColumnCellChannel can only bind to MiroGuideChannelInfo objects");
+            }
+
+            MiroGuideChannelInfo channel = (MiroGuideChannelInfo)BoundObject;
+
+            bool disable_border = false;
+
+            ImageSurface image = (artwork_manager == null) ? null
+                : artwork_manager.LookupScaleSurface (PaasService.ArtworkIdFor (channel.Name), image_size, true);
+
+            if (image == null) {
+                image = default_cover_image;
+                disable_border = true;
+            }
+
+            int image_render_size = image_size;
+            int x = image_spacing;
+            int y = ((int)cellHeight - image_render_size) / 2;
+
+            ArtworkRenderer.RenderThumbnail (context.Context, image, false, x, y,
+                image_render_size, image_render_size, !disable_border, context.Theme.Context.Radius, true
+            );
+
+            int fl_width = 0, fl_height = 0, sl_width = 0, sl_height = 0;
+            Cairo.Color text_color = context.Theme.Colors.GetWidgetColor (GtkColorClass.Text, state);
+            text_color.A = 0.75;
+
+            Pango.Layout layout = context.Layout;
+            layout.Width = (int)((cellWidth - cellHeight - x - 10) * Pango.Scale.PangoScale);
+            layout.Ellipsize = Pango.EllipsizeMode.End;
+            layout.FontDescription.Weight = Pango.Weight.Bold;
+
+            // Compute the layout sizes for both lines for centering on the cell
+            int old_size = layout.FontDescription.Size;
+
+            layout.SetText (channel.Name ?? String.Empty);
+            layout.GetPixelSize (out fl_width, out fl_height);
+
+            layout.FontDescription.Weight = Pango.Weight.Normal;
+            layout.FontDescription.Size = (int)(old_size * Pango.Scale.Small);
+            layout.FontDescription.Style = Pango.Style.Italic;
+
+            layout.SetText (channel.Publisher);
+
+            layout.GetPixelSize (out sl_width, out sl_height);
+
+            // Calculate the layout positioning
+            x = ((int)cellHeight - x) + 10;
+            y = (int)((cellHeight - (fl_height + sl_height)) / 2);
+
+            // Render the second line first since we have that state already
+            context.Context.MoveTo (x, y + fl_height);
+            context.Context.Color = text_color;
+            PangoCairoHelper.ShowLayout (context.Context, layout);
+
+            // Render the first line, resetting the state
+            //layout.SetText (channel.Name ?? String.Empty);
+            layout.FontDescription.Weight = Pango.Weight.Bold;
+            layout.FontDescription.Size = old_size;
+            layout.FontDescription.Style = Pango.Style.Normal;
+
+            layout.SetText (channel.Name ?? String.Empty);
+
+            context.Context.MoveTo (x, y);
+            text_color.A = 1;
+            context.Context.Color = text_color;
+            PangoCairoHelper.ShowLayout (context.Context, layout);
+        }
+
+        public int ComputeRowHeight (Widget widget)
+        {
+            int height;
+            int text_w, text_h;
+
+            Pango.Layout layout = new Pango.Layout (widget.PangoContext);
+            layout.FontDescription = widget.PangoContext.FontDescription.Copy ();
+
+            layout.SetText ("W");
+            layout.GetPixelSize (out text_w, out text_h);
+            height = text_h;
+
+            layout.FontDescription.Weight = Pango.Weight.Normal;
+            layout.FontDescription.Size = (int)(layout.FontDescription.Size * Pango.Scale.Small);
+            layout.FontDescription.Style = Pango.Style.Italic;
+            layout.SetText ("W");
+            layout.GetPixelSize (out text_w, out text_h);
+            height += text_h;
+
+            layout.Dispose ();
+
+            return (height < image_size ? image_size : height) + 6;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideAccountDialog.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideAccountDialog.cs
new file mode 100644
index 0000000..32a2be5
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideAccountDialog.cs
@@ -0,0 +1,153 @@
+//
+// MiroGuideAccountDialog.cs
+//
+// Authors:
+//   Aaron Bockover <abockover novell com>
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (C) 2006 Novell, Inc.
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+// This code has been copied/pasted so many times that it should probably just
+// be abstracted.
+
+using System;
+using Mono.Unix;
+
+using Gtk;
+using Banshee.Gui;
+
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class MiroGuideAccountDialog : Gtk.Dialog
+    {
+        private AccelGroup accel_group;
+        private MiroGuideLoginForm login_form;
+        private Label message;
+
+        public MiroGuideAccountDialog (MiroGuideAccountInfo accountInfo) : this (accountInfo, true)
+        {
+        }
+
+        public MiroGuideAccountDialog (MiroGuideAccountInfo accountInfo, bool addCloseButton) : base ()
+        {
+            Title = Catalog.GetString ("Miro Guide Account");
+            HasSeparator = false;
+            BorderWidth = 5;
+
+            IconName = "gtk-dialog-authentication";
+
+            accel_group = new AccelGroup ();
+            AddAccelGroup (accel_group);
+
+            HBox hbox = new HBox (false, 12);
+            VBox vbox = new VBox (false, 0);
+            hbox.BorderWidth = 5;
+            vbox.Spacing = 5;
+            hbox.Show ();
+            vbox.Show ();
+
+            Image image = new Image ();
+            image.Yalign = 0.0f;
+            image.Pixbuf = IconThemeUtils.LoadIcon (48, "miroguide");
+            image.IconSize = (int)IconSize.Dialog;
+            image.Show ();
+
+            hbox.PackStart (image, false, false, 0);
+            hbox.PackStart (vbox, true, true, 0);
+
+            Label header = new Label ();
+            header.Xalign = 0.0f;
+            header.Markup = String.Format ("<big><b>{0}</b></big>", Title);
+            header.Show ();
+
+            message = new Label (Catalog.GetString ("Please enter your Miro Guide account information."));
+            message.Xalign = 0.0f;
+            message.Show ();
+
+            vbox.PackStart (header, false, false, 0);
+            vbox.PackStart (message, false, false, 0);
+
+            login_form = new MiroGuideLoginForm (accountInfo);
+            login_form.Show ();
+
+            vbox.PackStart (login_form, true, true, 0);
+
+            VBox.PackStart (hbox, true, true, 0);
+            VBox.Remove (ActionArea);
+            VBox.Spacing = 10;
+
+            HBox bottom_box = new HBox ();
+
+            bottom_box.PackStart (ActionArea, true, true, 0);
+            bottom_box.ShowAll ();
+
+            VBox.PackEnd (bottom_box, false, false, 0);
+
+            if (addCloseButton) {
+                AddButton (Stock.Cancel, ResponseType.Cancel);
+                Button button = new Button (Stock.Save);
+                button.ShowAll ();
+
+                button.Activated += delegate {
+                    login_form.Save ();
+                };
+
+                button.Clicked += delegate {
+                    login_form.Save ();
+                };
+
+                AddActionWidget (button, ResponseType.Ok);
+                login_form.SaveOnEnter (this);
+            }
+        }
+
+        public void AddButton (string message, ResponseType response, bool isDefault)
+        {
+            Button button = (Button)AddButton (message, response);
+
+            if (isDefault) {
+                DefaultResponse = response;
+                button.AddAccelerator ("activate", accel_group, (uint)Gdk.Key.Return, 0, Gtk.AccelFlags.Visible);
+            }
+        }
+
+        public string Message {
+            get { return message.Text; }
+            set { message.Text = value; }
+        }
+
+        public bool SaveOnEdit {
+            get { return login_form.SaveOnEdit; }
+            set { login_form.SaveOnEdit = value; }
+        }
+
+        public string Username {
+            get { return login_form.Username; }
+        }
+
+        public string Password {
+            get { return login_form.Password; }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideActions.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideActions.cs
new file mode 100644
index 0000000..1e3f791
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideActions.cs
@@ -0,0 +1,197 @@
+//
+// MiroGuideActions.cs
+//
+// Author:
+//    Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Gtk;
+
+using Banshee.Gui;
+using Banshee.Base;
+using Banshee.Sources;
+using Banshee.ServiceStack;
+using Banshee.Configuration;
+
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class MiroGuideActions : BansheeActionGroup
+    {
+        private uint actions_id;
+        private MiroGuideClient client;
+
+        private ISource source;
+
+        private RadioAction active_action;
+        public RadioAction ActiveSortAction {
+            get { return active_action; }
+        }
+
+        public EventHandler<SortPreferenceChangedEventArgs> SortPreferenceChanged;
+
+        private ChannelSource ChannelSource {
+            get { return source as ChannelSource; }
+        }
+
+        public MiroGuideChannelListModel ActiveModel {
+            get { return (ChannelSource != null) ? ChannelSource.ChannelModel : null; }
+        }
+
+        public MiroGuideActions (MiroGuideClient client) : base (ServiceManager.Get<InterfaceActionService> (), "MiroGuide")
+        {
+            this.client = client;
+
+            AddImportant (
+                new ActionEntry (
+                    "MiroGuideRefreshChannelsAction", Stock.Refresh,
+                     Catalog.GetString ("Refresh"), null, null,
+                     (sender, e) => { ChannelSource.Refresh (); }
+                )
+            );
+
+            Add (new ActionEntry [] {
+                new ActionEntry (
+                   "MiroGuideChannelPopupAction", null, null, null, null, OnChannelPopup
+                ), new ActionEntry (
+                    "MiroGuideChannelSubscribeAction", Stock.Add,
+                    Catalog.GetString ("Subscribe"), null,
+                    null, OnMiroGuideChannelSubscribeHandler
+                ), new ActionEntry (
+                    "PaasEditMiroGuidePropertiesAction", Stock.Properties,
+                    Catalog.GetString ("Edit Miro Guide Settings"), "<control>M",
+                    null, (sender, e) => {
+                        MiroGuideAccountDialog mgad = new MiroGuideAccountDialog (PaasService.MiroGuideAccount);
+                        mgad.Run ();
+                        mgad.Destroy ();
+                    }
+                )
+            });
+
+            Add (new RadioActionEntry [] {
+                new RadioActionEntry ("MiroGuideSortByNameAction", null,
+                    Catalog.GetString ("Sort Channels by Name"), null,
+                    Catalog.GetString ("Order results by name."),
+                    (int)MiroGuideSortType.Name),
+
+                new RadioActionEntry ("MiroGuideSortByRelevanceAction", null,
+                    Catalog.GetString ("Sort Channels by Relevance"), null,
+                    Catalog.GetString ("Order results by relevance."),
+                    (int)MiroGuideSortType.Relevance),
+
+                new RadioActionEntry ("MiroGuideSortByRatingAction", null,
+                    Catalog.GetString ("Sort Channels by Rating"), null,
+                    Catalog.GetString ("Order results by rating."),
+                    (int)MiroGuideSortType.Rating),
+
+                new RadioActionEntry ("MiroGuideSortByPopularityAction", null,
+                    Catalog.GetString ("Sort Channels by Popularity"), null,
+                    Catalog.GetString ("Order results by popularity."),
+                    (int)MiroGuideSortType.Popular)
+            }, 0, OnActionChangedHandler);
+
+            SetActiveSortPreference (MiroGuideSortType.Name);
+
+            actions_id = Actions.UIManager.AddUiFromResource ("MiroGuideUI.xml");
+            Actions.AddActionGroup (this);
+
+            ServiceManager.SourceManager.ActiveSourceChanged += (e) => {
+                source = e.Source;
+            };
+        }
+
+        public override void Dispose ()
+        {
+            Actions.UIManager.RemoveUi (actions_id);
+            Actions.RemoveActionGroup (this);
+            base.Dispose ();
+        }
+
+        public void SetActiveSortPreference (MiroGuideSortType sort)
+        {
+            SetActiveSortPreference (sort, true);
+        }
+
+        public void SetActiveSortPreference (MiroGuideSortType sort, bool raise)
+        {
+            if (active_action == null || (int)sort != active_action.Value) {
+                active_action = GetSortPreferenceAction (sort);
+                active_action.Active = true;
+
+                if (raise) {
+                    OnSortPreferenceChanged (sort);
+                }
+            }
+        }
+
+        private IEnumerable<MiroGuideChannelInfo> GetSelectedChannels ()
+        {
+            return new List<MiroGuideChannelInfo> (ChannelSource.ChannelModel.GetSelected ());
+        }
+
+        private void OnMiroGuideChannelSubscribeHandler (object sender, EventArgs e)
+        {
+            client.RequestSubsubscription (GetSelectedChannels ().Select (c => new Uri (c.Url)));
+        }
+
+        private void OnChannelPopup (object sender, EventArgs e)
+        {
+            ShowContextMenu ("/MiroGuideChannelPopup");
+        }
+
+        private RadioAction GetSortPreferenceAction (MiroGuideSortType sort)
+        {
+            switch (sort) {
+            case MiroGuideSortType.Name: return GetAction ("MiroGuideSortByNameAction") as RadioAction;
+            case MiroGuideSortType.Rating: return GetAction ("MiroGuideSortByRatingAction") as RadioAction;
+            case MiroGuideSortType.Popular: return GetAction ("MiroGuideSortByPopularityAction") as RadioAction;
+            case MiroGuideSortType.Relevance: return GetAction ("MiroGuideSortByRelevanceAction") as RadioAction;
+            default:
+                goto case MiroGuideSortType.Name;
+            }
+        }
+
+        private void OnSortPreferenceChanged (MiroGuideSortType sort)
+        {
+            var handler = SortPreferenceChanged;
+
+            if (handler != null) {
+                handler (null, new SortPreferenceChangedEventArgs (ChannelSource, sort));
+            }
+        }
+
+        private void OnActionChangedHandler (object o, ChangedArgs args)
+        {
+            if (active_action != args.Current) {
+                active_action = args.Current;
+                OnSortPreferenceChanged ((MiroGuideSortType)active_action.Value);
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideCategoryListView.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideCategoryListView.cs
new file mode 100644
index 0000000..2f15565
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideCategoryListView.cs
@@ -0,0 +1,51 @@
+//
+// MiroGuideCategoryListView.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Mono.Unix;
+
+using Hyena.Data.Gui;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+using Banshee.Collection.Gui;
+
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class MiroGuideCategoryListView : ListView<MiroGuideCategoryInfo>
+    {
+        public MiroGuideCategoryListView ()
+        {
+            ColumnCellText name_renderer = new ColumnCellText ("Name", true);
+
+            ColumnController = new ColumnController ();
+            ColumnController.Add (new Column (Catalog.GetString ("Categories"), name_renderer, 1.0));
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideChannelListView.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideChannelListView.cs
new file mode 100644
index 0000000..cb64f85
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideChannelListView.cs
@@ -0,0 +1,63 @@
+//
+// MiroGuideChannelListView.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Hyena.Data.Gui;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+using Banshee.Collection.Gui;
+
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class MiroGuideChannelListView : ListView<MiroGuideChannelInfo>
+    {
+        private ColumnCellChannel renderer;
+
+        public MiroGuideChannelListView ()
+        {
+            renderer = new ColumnCellChannel ();
+
+            ColumnController column_controller = new ColumnController ();
+            column_controller.Add (new Column ("Channels", renderer, 1.0));
+
+            ColumnController  = column_controller;
+            RowHeightProvider = renderer.ComputeRowHeight;
+        }
+
+        protected override bool OnPopupMenu ()
+        {
+            ServiceManager.Get<InterfaceActionService> ().FindAction (
+                "MiroGuide.MiroGuideChannelPopupAction"
+            ).Activate ();
+
+            return true;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideLoginForm.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideLoginForm.cs
new file mode 100644
index 0000000..a1c36eb
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideLoginForm.cs
@@ -0,0 +1,143 @@
+//
+// MiroGuideLoginForm.cs
+//
+// Authors:
+//   Aaron Bockover <abockover novell com>
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (C) 2006 Novell, Inc.
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Mono.Unix;
+
+using Gtk;
+
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class MiroGuideLoginForm : Gtk.Table
+    {
+        private MiroGuideAccountInfo account_info;
+        private Entry username_entry;
+        private Entry password_entry;
+
+        private bool save_on_edit = false;
+
+        private bool save_on_enter = false;
+        private Gtk.Dialog parentDialog;
+
+        public MiroGuideLoginForm (MiroGuideAccountInfo accountInfo) : base (2, 2, false)
+        {
+            this.account_info = accountInfo;
+
+            BorderWidth = 5;
+            RowSpacing = 5;
+            ColumnSpacing = 5;
+
+            Label username_label = new Label (Catalog.GetString ("Username:"));
+            username_label.Xalign = 1.0f;
+            username_label.Show ();
+
+            username_entry = new Entry ();
+            username_entry.Show ();
+
+            Label password_label = new Label (Catalog.GetString ("Password:"));
+            password_label.Xalign = 1.0f;
+            password_label.Show ();
+
+            password_entry = new Entry ();
+            password_entry.Visibility = false;
+
+            //When the user presses enter in the password field: save, and then destroy the AcountLoginDialog
+            password_entry.Activated += delegate {
+                if (save_on_enter) {
+                    this.Save ();
+                    parentDialog.Destroy ();
+                }
+            };
+
+            password_entry.Show ();
+
+            Attach (username_label, 0, 1, 0, 1, AttachOptions.Fill,
+                AttachOptions.Shrink, 0, 0);
+
+            Attach (username_entry, 1, 2, 0, 1, AttachOptions.Fill | AttachOptions.Expand,
+                AttachOptions.Shrink, 0, 0);
+
+            Attach (password_label, 0, 1, 1, 2, AttachOptions.Fill,
+                AttachOptions.Shrink, 0, 0);
+
+            Attach (password_entry, 1, 2, 1, 2, AttachOptions.Fill | AttachOptions.Expand,
+                AttachOptions.Shrink, 0, 0);
+
+            username_entry.Text = account_info.Username ?? String.Empty;
+            password_entry.Text = account_info.PasswordHash ?? String.Empty;
+        }
+
+        protected override void OnDestroyed ()
+        {
+            if (save_on_edit) {
+                Save ();
+            }
+
+            base.OnDestroyed ();
+        }
+
+        public void Save ()
+        {
+            string trimmed_user = username_entry.Text.Trim ();
+            string trimmed_password_hash = password_entry.Text.Trim ();
+
+            if (!Hyena.CryptoUtil.IsMd5Encoded (trimmed_password_hash)) {
+                trimmed_password_hash = Hyena.CryptoUtil.Md5Encode (trimmed_password_hash);
+            }
+
+            if (account_info.Username != trimmed_user || account_info.PasswordHash != trimmed_password_hash) {
+                account_info.Username = trimmed_user;
+                account_info.PasswordHash = trimmed_password_hash;
+                account_info.Notify ();
+            }
+        }
+
+        public bool SaveOnEdit {
+            get { return save_on_edit; }
+            set { save_on_edit = value; }
+        }
+
+        //enable save on Enter and destruction of the parentDialog.
+        public void SaveOnEnter (Gtk.Dialog parentDialog)
+        {
+           save_on_enter = true;
+           this.parentDialog = parentDialog;
+        }
+
+        public string Username {
+            get { return username_entry.Text; }
+        }
+
+        public string Password {
+            get { return password_entry.Text; }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideSearchEntry.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideSearchEntry.cs
new file mode 100644
index 0000000..29a4ac1
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideSearchEntry.cs
@@ -0,0 +1,55 @@
+//
+// MiroGuideSearchEntry.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Mono.Unix;
+
+using Banshee.Widgets;
+
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class MiroGuideSearchEntry : SearchEntry
+    {
+        public MiroGuideSearchEntry ()
+        {
+            string default_str = Catalog.GetString ("Search Miro Guide");
+            EmptyMessage = default_str;
+
+// MG doesn't support search filtering by any type other than hd
+/*
+            AddFilterOption ((int)MiroGuideSearchFilter.Default, default_str);
+
+            AddFilterSeparator ();
+            AddFilterOption ((int)MiroGuideSearchFilter.HD, Catalog.GetString ("HD Channels"));
+            AddFilterOption ((int)MiroGuideSearchFilter.Video, Catalog.GetString ("Video Channels"));
+            AddFilterOption ((int)MiroGuideSearchFilter.Audio, Catalog.GetString ("Audio Channels"));
+*/
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ReflectionInfoWidget.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ReflectionInfoWidget.cs
new file mode 100644
index 0000000..8e72c36
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ReflectionInfoWidget.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Cairo;
+using Gdk;
+using Gtk;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public abstract class AbstractInfoRenderer
+    {
+        int maxWidth;
+        Style style;
+
+        public int MaxWidth {
+            get { return maxWidth; }
+            set {
+                maxWidth = value;
+                OnMaxWidthSet (maxWidth);
+            }
+        }
+
+        public Style Style {
+            get { return Style; }
+            set {
+                style = value;
+                OnStyleSet (style);
+            }
+        }
+
+        public virtual int HeightRequest {
+            get { return 0; }
+        }
+
+        public virtual int WidthRequest {
+            get { return MaxWidth; }
+        }
+
+        protected virtual void OnMaxWidthSet (int width)
+        {
+            // do shit here
+        }
+
+        protected virtual void OnStyleSet (Style style)
+        {
+            // do shit here
+        }
+
+        public virtual void RenderOntoContext (Context cr, int x, int y)
+        {
+        }
+
+    }
+
+    public class TitleInfoRenderer : AbstractInfoRenderer
+    {
+        string text;
+
+        public override int HeightRequest {
+            get { return 32; }
+        }
+
+        public TitleInfoRenderer (string text)
+        {
+            this.text = text;
+        }
+
+        public override void RenderOntoContext (Context cr, int x, int y)
+        {
+            Pango.Layout layout = Pango.CairoHelper.CreateLayout (cr);
+            layout.FontDescription = Style.FontDescription;
+            layout.FontDescription.AbsoluteSize = 30;
+            layout.FontDescription.Weight = Pango.Weight.Bold;
+
+            layout.Ellipsize = Pango.EllipsizeMode.End;
+            layout.SetText (text);
+            layout.Width = Pango.Units.ToPixels (MaxWidth);
+            cr.MoveTo (x, y);
+            Pango.CairoHelper.LayoutPath (cr, layout);
+            cr.Color = new Cairo.Color (1, 1, 1);
+            cr.Fill ();
+        }
+    }
+
+    public class ReflectionInfoWidget : DrawingArea
+    {
+        const double TopBorder = .2;
+        const double ImageSize = .8;
+        const double LeftRightSeparation = .5;
+        const int Separation = 10;
+
+        public double YAlign { get; set; }
+        public double XAlign { get; set; }
+
+        public ImageSurface ReflectionImage { get; set; }
+
+        IEnumerable<AbstractInfoRenderer> renderers;
+        public IEnumerable<AbstractInfoRenderer> Renderers {
+            get { return renderers; }
+            set {
+                renderers = value.ToArray (); // prevent it from changing due to lazy eval
+                if (Style == null)
+                    return;
+                foreach (AbstractInfoRenderer renderer in renderers) {
+                    renderer.Style = Style;
+                }
+            }
+        }
+
+        public ReflectionInfoWidget ()
+        {
+            renderers = new List<AbstractInfoRenderer> ();
+
+            AppPaintable = true;
+            DoubleBuffered = true;
+        }
+
+        protected override void OnStyleSet (Gtk.Style previous_style)
+        {
+            foreach (AbstractInfoRenderer renderer in Renderers) {
+                renderer.Style = Style;
+            }
+            base.OnStyleSet (previous_style);
+        }
+
+        protected override bool OnExposeEvent (Gdk.EventExpose evnt)
+        {
+            if (!IsRealized || ReflectionImage == null)
+                return base.OnExposeEvent (evnt);
+
+            using (Cairo.Context cr = Gdk.CairoHelper.Create (evnt.Window)) {
+                cr.Operator = Operator.Source;
+                cr.Color = new Cairo.Color (0, 0, 0);
+                cr.Paint ();
+                cr.Operator = Operator.Over;
+
+                int midline = Allocation.Width / 2;
+                int topline = (int) (Allocation.Height * TopBorder);
+                int leftWidth = midline - 2 * Separation;
+
+                cr.SetSource (ReflectionImage, midline + Separation, topline);
+                cr.Paint ();
+
+                foreach (AbstractInfoRenderer renderer in Renderers) {
+                    renderer.MaxWidth = leftWidth;
+                }
+
+                int totalHeight = Renderers.Sum (r => r.HeightRequest);
+
+                int y = (int) (topline + (ReflectionImage.Height - totalHeight) * YAlign);
+
+                foreach (AbstractInfoRenderer renderer in Renderers) {
+                    int x = (int) (Separation + (leftWidth - renderer.WidthRequest) * XAlign);
+                    renderer.RenderOntoContext (cr, x, y);
+                    y += renderer.HeightRequest;
+                }
+            }
+
+            return true;
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SortPreferenceActionButton.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SortPreferenceActionButton.cs
new file mode 100644
index 0000000..7951aec
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SortPreferenceActionButton.cs
@@ -0,0 +1,79 @@
+//
+// SortPreferenceMenuButton.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using Gtk;
+using System;
+
+using Mono.Unix;
+
+using Hyena.Widgets;
+
+using Banshee.Gui;
+using Banshee.ServiceStack;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class SortPreferenceActionButton : EventBox
+    {
+        private MenuButton button;
+        private HBox box = new HBox ();
+        private Label label = new Label ();
+        private MiroGuideActions actions;
+
+        public SortPreferenceActionButton ()
+        {
+            UIManager ui_manager = ServiceManager.Get <InterfaceActionService> ().UIManager;
+            actions = ServiceManager.Get <InterfaceActionService> ().FindActionGroup ("MiroGuide") as MiroGuideActions;
+
+            actions.SortPreferenceChanged += OnActionChanged;
+
+            box.Spacing = 0;
+
+            label.UseUnderline = true;
+            box.PackStart (label, true, true, 6);
+
+            button = new MenuButton (box, ui_manager.GetWidget ("/MiroGuideSortPreferencePopup") as Menu, true);
+            Add (button);
+
+            UpdateButton ();
+
+            ShowAll ();
+        }
+
+        private void UpdateButton ()
+        {
+            button.Sensitive = label.Sensitive = actions.Sensitive;
+            label.TextWithMnemonic = actions.ActiveSortAction.Label;
+        }
+
+        private void OnActionChanged (object o, EventArgs args)
+        {
+            UpdateButton ();
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SortPreferenceChangedEventArgs.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SortPreferenceChangedEventArgs.cs
new file mode 100644
index 0000000..7ccac5c
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SortPreferenceChangedEventArgs.cs
@@ -0,0 +1,51 @@
+//
+// SortPreferenceChangedEventArgs.cs
+//
+// Author:
+//    Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Banshee.Sources;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class SortPreferenceChangedEventArgs : EventArgs
+    {
+        private readonly MiroGuideSortType sort;
+        private readonly ChannelSource active_source;
+
+        public ChannelSource ActiveSource {
+            get { return active_source; }
+        }
+
+        public MiroGuideSortType Sort {
+            get { return sort; }
+        }
+
+        public SortPreferenceChangedEventArgs (ChannelSource source, MiroGuideSortType sort)
+        {
+            active_source = source;
+            this.sort = sort;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/BrowserSourceContents.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/BrowserSourceContents.cs
new file mode 100644
index 0000000..16831a0
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/BrowserSourceContents.cs
@@ -0,0 +1,98 @@
+//
+// BrowserSourceContents.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Gtk;
+
+using Banshee.Sources;
+using Banshee.Configuration;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class BrowserSourceContents : ChannelSourceContents
+    {
+        private VPaned browser_pane;
+        private double category_view_vadjustment;
+        private MiroGuideCategoryListView category_view;
+
+        public MiroGuideCategoryListView CategoryListView {
+            get { return category_view; }
+        }
+
+        public BrowserSourceContents ()
+        {
+            browser_pane = new VPaned ();
+            category_view = new MiroGuideCategoryListView ();
+
+            ScrolledWindow category_sw = new ScrolledWindow () {
+                HscrollbarPolicy = PolicyType.Automatic,
+                VscrollbarPolicy = PolicyType.Automatic
+            };
+
+            category_sw.Add (category_view);
+
+            browser_pane.Add1 (ScrolledWindow);
+            browser_pane.Add2 (category_sw);
+
+            browser_pane.Position = BrowserSourceVPanedPosition.Get ();
+
+            browser_pane.SizeAllocated += (sender, e) => {
+                BrowserSourceVPanedPosition.Set (browser_pane.Position);
+            };
+        }
+
+        public override void Initialize ()
+        {
+            FilterBox.Add (browser_pane);
+            Widget.ShowAll ();
+        }
+
+        public override bool SetSource (ISource source)
+        {
+            BrowseChannelsSource s = source as BrowseChannelsSource;
+
+            if (base.SetSource (source) && s != null) {
+                category_view.SetModel (s.CategoryModel, category_view_vadjustment);
+                browser_pane.Position = BrowserSourceVPanedPosition.Get ();
+                return true;
+            }
+
+            return false;
+        }
+
+        public override void ResetSource ()
+        {
+            category_view_vadjustment = category_view.Vadjustment.Value;
+            category_view.SetModel (null);
+            base.ResetSource ();
+        }
+
+        public static readonly SchemaEntry<int> BrowserSourceVPanedPosition = new SchemaEntry<int> (
+            "plugins.paas.miroguide.ui", "browser_source_vpaned_pos", 250, "", ""
+        );
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/ChannelSourceContents.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/ChannelSourceContents.cs
new file mode 100644
index 0000000..35eb621
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/ChannelSourceContents.cs
@@ -0,0 +1,183 @@
+//
+// ChannelSourceContents.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+using Gtk;
+
+using Banshee.Gui;
+using Banshee.Sources.Gui;
+
+using Banshee.Base;
+using Banshee.Sources;
+using Banshee.ServiceStack;
+using Banshee.Configuration;
+
+using Banshee.Paas.Data;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class ChannelSourceContents : ISourceContents
+    {
+        private HPaned hp;
+        private VBox filter_box;
+        private ScrolledWindow sw;
+
+        private ChannelSource source;
+        private SingletonSelection selection;
+        private ChannelInfoPreview channel_preview;
+        private MiroGuideChannelListView channel_view;
+
+        private double channel_view_vadjustment;
+
+        public MiroGuideChannelListView ChannelView {
+            get { return channel_view; }
+        }
+
+        public ISource Source {
+            get { return source; }
+        }
+
+        public Widget Widget {
+            get { return hp as Widget; }
+        }
+
+        public VBox FilterBox {
+            get { return filter_box; }
+        }
+
+        public ScrolledWindow ScrolledWindow {
+            get { return sw; }
+        }
+
+        private static SortPreferenceActionButton sb;
+
+        static ChannelSourceContents ()
+        {
+            sb = new SortPreferenceActionButton ();
+            sb.Hide ();
+
+            ServiceManager.Get<InterfaceActionService> ().PopulateToolbarPlaceholder (
+                (Toolbar)ServiceManager.Get<InterfaceActionService> ().UIManager.GetWidget ("/FooterToolbar"),
+                "/FooterToolbar/Extensions/MiroGuideChannelSortButton",
+                sb
+            );
+        }
+
+        public ChannelSourceContents ()
+        {
+            channel_view = new MiroGuideChannelListView ();
+
+            sw = new ScrolledWindow () {
+                HscrollbarPolicy = PolicyType.Automatic,
+                VscrollbarPolicy = PolicyType.Automatic
+            };
+
+            sw.Add (channel_view);
+
+            hp = new HPaned ();
+
+            hp.SizeAllocated += (sender, e) => {
+                HPanedPosition.Set (hp.Position);
+            };
+
+            filter_box = new VBox ();
+            channel_preview = new ChannelInfoPreview ();
+
+            hp.Add1 (filter_box);
+            hp.Add2 (channel_preview);
+        }
+
+        public virtual void Initialize ()
+        {
+            filter_box.Add (sw);
+            Widget.ShowAll ();
+        }
+
+        public virtual bool SetSource (ISource source)
+        {
+            this.source = source as ChannelSource;
+
+            if (this.source == null) {
+                return false;
+            }
+
+            selection = this.source.ChannelModel.Selection as SingletonSelection;
+            selection.Changed += OnSelectionChanged;
+
+            channel_view.HeaderVisible = true;
+            hp.Position = HPanedPosition.Get ();
+            channel_view.SetModel (this.source.ChannelModel, channel_view_vadjustment);
+
+            if (this.source.Properties.Get<bool> ("MiroGuide.Gui.Source.ShowSortPreference")) {
+                ShowSortPreferenceButton ();
+            }
+
+            return true;
+        }
+
+        public virtual void ResetSource ()
+        {
+            selection.Changed -= OnSelectionChanged;
+            selection = null;
+
+            source = null;
+            channel_view_vadjustment = channel_view.Vadjustment.Value;
+
+            channel_view.SetModel (null);
+            channel_view.HeaderVisible = false;
+
+            HideSortPreferenceButton ();
+        }
+
+        private void OnSelectionChanged (object sender, EventArgs e)
+        {
+            channel_preview.ChannelInfo = source.ChannelModel.Selected;
+        }
+
+        public static void ShowSortPreferenceButton ()
+        {
+            sb.Show ();
+        }
+
+        public static void HideSortPreferenceButton ()
+        {
+            sb.Hide ();
+        }
+
+        public static bool SortPreferenceButtonSensitive {
+            get { return sb.Sensitive; }
+            set { sb.Sensitive = value;  }
+        }
+
+        public static readonly SchemaEntry<int> HPanedPosition = new SchemaEntry<int> (
+            "plugins.paas.miroguide.ui", "channel_source_hpaned_pos", 255, "", ""
+        );
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/MiroGuideSourceContents.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/MiroGuideSourceContents.cs
new file mode 100644
index 0000000..96b6974
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/MiroGuideSourceContents.cs
@@ -0,0 +1,71 @@
+//
+// MiroGuideSourceContents.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Gdk;
+using Gtk;
+
+using Banshee.Sources;
+using Banshee.Sources.Gui;
+
+namespace Banshee.Paas.MiroGuide.Gui
+{
+    public class MiroGuideSourceContents : ISourceContents
+    {
+        private VBox widget;
+        private MiroGuideSource source;
+
+        public ISource Source {
+            get { return source; }
+        }
+
+        public Widget Widget {
+            get { return widget as Widget; }
+        }
+
+        public MiroGuideSourceContents ()
+        {
+            widget = new VBox ();
+        }
+
+        public bool SetSource (ISource source)
+        {
+            this.source = source as MiroGuideSource;
+
+            if (source == null) {
+                return false;
+            }
+
+            return true;
+        }
+
+        public void ResetSource ()
+        {
+            source = null;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideCategoryListModel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideCategoryListModel.cs
new file mode 100644
index 0000000..69dda88
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideCategoryListModel.cs
@@ -0,0 +1,41 @@
+//
+// MiroGuideCategoryListModel.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Banshee.Paas.Data;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class MiroGuideCategoryListModel : ListModel<MiroGuideCategoryInfo>
+    {
+        public MiroGuideCategoryListModel () : base (new SingletonSelection ())
+        {
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideChannelListModel.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideChannelListModel.cs
new file mode 100644
index 0000000..6c2e848
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideChannelListModel.cs
@@ -0,0 +1,46 @@
+//
+// MiroGuideChannelListModel.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+using Banshee.Paas.Data;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class MiroGuideChannelListModel : ListModel<MiroGuideChannelInfo>
+    {
+        public MiroGuideChannelInfo Selected {
+            get { return GetSelected ().DefaultIfEmpty ().First (); }
+        }
+
+        public MiroGuideChannelListModel () : base (new SingletonSelection ())
+        {
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideImageFetchJob.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideImageFetchJob.cs
new file mode 100644
index 0000000..db0ee75
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideImageFetchJob.cs
@@ -0,0 +1,86 @@
+//
+// MiroGuideImageFetchJob.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+
+using System;
+
+using Hyena;
+
+using Banshee.Base;
+using Banshee.Metadata;
+using Banshee.ServiceStack;
+
+using Banshee.Paas.MiroGuide.Gui;
+using Banshee.Paas.Aether.MiroGuide;
+
+// This could be abstracted and shared with PaasImageFetchJob...
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class MiroGuideImageFetchJob : MetadataServiceJob
+    {
+        private MiroGuideChannelInfo channel;
+
+        public MiroGuideImageFetchJob (MiroGuideChannelInfo channel) : base ()
+        {
+            this.channel = channel;
+        }
+
+        public override void Run ()
+        {
+            Fetch ();
+        }
+
+        public void Fetch ()
+        {
+            if (String.IsNullOrEmpty (channel.ThumbUrl)) {
+                return;
+            }
+
+            string cover_art_id = PaasService.ArtworkIdFor (channel.Name);
+
+            if (cover_art_id == null) {
+                return;
+            } else if (CoverArtSpec.CoverExists (cover_art_id)) {
+                return;
+            } else if (!InternetConnected) {
+                return;
+            }
+
+            if (SaveHttpStreamCover (new Uri (channel.ThumbUrl), cover_art_id, null)) {
+                Banshee.Sources.Source src = ServiceManager.SourceManager.ActiveSource;
+
+                if (src != null && (src is ChannelSource || src.Parent is ChannelSource)) {
+                    (src as ChannelSource).QueueDraw ();
+                }
+
+                return;
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideInterfaceManager.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideInterfaceManager.cs
new file mode 100644
index 0000000..4e78e11
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideInterfaceManager.cs
@@ -0,0 +1,74 @@
+//
+// MiroGuideInterfaceManager.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Banshee.ServiceStack;
+using Banshee.Paas.Aether.MiroGuide;
+
+using Banshee.Paas.MiroGuide.Gui;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class MiroGuideInterfaceManager : IDisposable
+    {
+        private MiroGuideActions actions;
+        private MiroGuideSource mg_source;
+
+        public MiroGuideInterfaceManager ()
+        {
+        }
+
+        public void Initialize (MiroGuideClient client)
+        {
+            actions = new MiroGuideActions (client);
+
+            mg_source = new MiroGuideSource (client);
+            mg_source.AddChildSource (new SearchSource           (client));
+            mg_source.AddChildSource (new HDChannelsSource       (client));
+            mg_source.AddChildSource (new FeaturedChannelsSource (client));
+            mg_source.AddChildSource (new PopularChannelsSource  (client));
+            mg_source.AddChildSource (new TopRatedChannelsSource (client));
+            mg_source.AddChildSource (new BrowseChannelsSource   (client));
+            //mg_source.AddChildSource (new RecommendedChannelsSource (client));
+
+            ServiceManager.SourceManager.AddSource (mg_source);
+        }
+
+        public void Dispose ()
+        {
+            if (mg_source != null) {
+                ServiceManager.SourceManager.RemoveSource (mg_source);
+                mg_source = null;
+            }
+
+            if (actions != null) {
+                actions.Dispose ();
+                actions = null;
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideSearchFilter.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideSearchFilter.cs
new file mode 100644
index 0000000..3c77b98
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideSearchFilter.cs
@@ -0,0 +1,13 @@
+
+using System;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public enum MiroGuideSearchFilter : int
+    {
+        Default = 0,
+        HD,
+        Video,
+        Audio
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/BrowseChannelsSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/BrowseChannelsSource.cs
new file mode 100644
index 0000000..a34a521
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/BrowseChannelsSource.cs
@@ -0,0 +1,135 @@
+//
+// BrowseChannelsSource.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Linq;
+
+using Mono.Unix;
+
+using Gtk;
+
+using Banshee.Base;
+
+using Banshee.Paas.Data;
+using Banshee.Paas.MiroGuide.Gui;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class BrowseChannelsSource : ChannelSource
+    {
+        private bool categories_received;
+        private MiroGuideCategoryListModel category_model;
+
+        public MiroGuideCategoryListModel CategoryModel {
+            get { return category_model; }
+        }
+
+        public BrowseChannelsSource (MiroGuideClient client) : base (client,
+                                                                     MiroGuideFilterType.Category,
+                                                                     "MiroGuideBrowseChannels",
+                                                                     Catalog.GetString ("Browse"),
+                                                                     (int)MiroGuideSourcePosition.Browse)
+        {
+            ActiveSortType = MiroGuideSortType.Rating;
+            category_model = new MiroGuideCategoryListModel ();
+
+            Properties.SetStringList ("Icon.Name", "miro-browse");
+            Properties.Set<bool> ("MiroGuide.Gui.Source.ShowSortPreference", true);
+        }
+
+        public override void Activate ()
+        {
+            base.Activate ();
+
+            Client.GetCategoriesCompleted += OnGetCategoriesCompletedHandler;
+            category_model.Selection.Changed += OnSelectionChangedHandler;
+
+            if (!categories_received) {
+                Client.GetCategoriesAsync (this);
+            }
+        }
+
+        public override void Deactivate ()
+        {
+            Client.GetCategoriesCompleted -= OnGetCategoriesCompletedHandler;
+            category_model.Selection.Changed -= OnSelectionChangedHandler;
+
+            base.Deactivate ();
+        }
+
+        protected override ChannelSourceContents CreateChannelSourceContents ()
+        {
+            return new BrowserSourceContents ();
+        }
+
+        public override void Refresh ()
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                if (!categories_received) {
+                    Client.GetCategoriesAsync (this);
+                }
+
+                base.Refresh ();
+            });
+        }
+
+        private MiroGuideCategoryInfo selected_category = null;
+        protected virtual void OnSelectionChangedHandler (object sender, EventArgs e)
+        {
+            MiroGuideCategoryInfo category = category_model.GetSelected ().FirstOrDefault ();
+
+            if (category != null && category != selected_category) {
+                Client.CancelAsync ();
+                ClientHandle.WaitOne ();
+
+                GetChannelsAsync (category.Name);
+                selected_category = category;
+            }
+        }
+
+        protected virtual void OnGetCategoriesCompletedHandler (object sender, GetCategoriesCompletedEventArgs e)
+        {
+            if (e.UserState != this) {
+                return;
+            }
+
+            ThreadAssist.ProxyToMain (delegate {
+                if (e.Cancelled) {
+                    return;
+                } else if (e.Error != null) {
+                    SetErrorStatus ();
+                    return;
+                }
+
+                if (e.Categories != null) {
+                    CategoryModel.Add (e.Categories);
+                    categories_received = true;
+                }
+            });
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/ChannelSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/ChannelSource.cs
new file mode 100644
index 0000000..7d95cfa
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/ChannelSource.cs
@@ -0,0 +1,419 @@
+//
+// ChannelSource.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Threading;
+
+using System.Linq;
+using System.Collections.Generic;
+
+using Gtk;
+
+using Mono.Unix;
+
+using Banshee.Gui;
+using Banshee.Base;
+using Banshee.Sources;
+using Banshee.Sources.Gui;
+using Banshee.ServiceStack;
+
+using Banshee.Paas.Aether;
+using Banshee.Paas.Aether.MiroGuide;
+
+using Banshee.Paas.MiroGuide.Gui;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public abstract class ChannelSource : Source, IDisposable
+    {
+        private bool ignore_scroll;
+        private MiroGuideClient client;
+        private MiroGuideActions actions;
+
+        private SourceMessage status_message;
+        private SourceMessage error_status_message;
+
+        private ChannelSourceContents contents;
+        private MiroGuideChannelListModel channel_model;
+
+        private MiroGuideSortType active_sort_type;
+        private readonly MiroGuideFilterType filter_type;
+
+        protected MiroGuideSortType ActiveSortType {
+            get { return active_sort_type; }
+            set { active_sort_type = value; }
+        }
+
+        protected MiroGuideActions Actions {
+            get { return actions; }
+        }
+
+        protected MiroGuideClient Client {
+            get { return client; }
+        }
+
+        protected ChannelSourceContents Contents {
+            get { return contents; }
+        }
+
+        protected string BusyStatusMessage { get; set; }
+        protected SearchContext Context { get; set; }
+
+        public MiroGuideChannelListModel ChannelModel {
+            get { return channel_model; }
+        }
+
+        private static ManualResetEvent client_handle = new ManualResetEvent (true);
+        protected static ManualResetEvent ClientHandle {
+            get { return client_handle; }
+        }
+
+        public ChannelSource (MiroGuideClient client,
+                              MiroGuideFilterType filterType,
+                              string genericName, string name, int order) : base (genericName, name, order)
+        {
+            if (client == null) {
+                throw new ArgumentNullException ("client");
+            }
+
+            this.client = client;
+            this.filter_type = filterType;
+
+            active_sort_type = MiroGuideSortType.Name;
+            actions = ServiceManager.Get <InterfaceActionService> ().FindActionGroup ("MiroGuide") as MiroGuideActions;
+
+            BusyStatusMessage = Catalog.GetString ("Updating");
+
+            channel_model = new MiroGuideChannelListModel ();
+
+            channel_model.Cleared  += (sender, e) => { OnUpdated (); };
+            channel_model.Reloaded += (sender, e) => { QueueDraw (); OnUpdated (); };
+
+            this.client.StateChanged += OnMiroGuideClientStateChanged;
+
+            TypeUniqueId = String.Concat (genericName, "ChannelSource");
+
+            Properties.SetString ("ActiveSourceUIResource", "MiroGuideActiveSourceUI.xml");
+            Properties.Set<bool> ("ActiveSourceUIResourcePropagate", false);
+            Properties.Set<System.Reflection.Assembly> ("ActiveSourceUIResource.Assembly", typeof(ChannelSource).Assembly);
+
+            Properties.SetString ("GtkActionPath", "/MiroGuideChannelSourcePopup");
+
+            contents = CreateChannelSourceContents ();
+            contents.Initialize ();
+
+            (contents.ScrolledWindow.VScrollbar as VScrollbar).ValueChanged += OnVScrollbarValueChangedHandler;
+
+            Properties.Set<ISourceContents> ("Nereid.SourceContents", contents);
+            Properties.Set<bool> ("Nereid.SourceContentsPropagate", false);
+        }
+
+        public override void Activate ()
+        {
+            ClientHandle.WaitOne ();
+
+            if (active_sort_type != MiroGuideSortType.None) {
+                actions.SetActiveSortPreference (active_sort_type);
+            }
+
+            client.GetChannelsCompleted += OnGetChannelsCompletedHandler;
+            actions.SortPreferenceChanged += OnSortPreferenceChangedHandler;
+        }
+
+        public override void Deactivate ()
+        {
+            client.GetChannelsCompleted -= OnGetChannelsCompletedHandler;
+            actions.SortPreferenceChanged -= OnSortPreferenceChangedHandler;
+            client.CancelAsync ();
+        }
+
+        public virtual void Dispose ()
+        {
+        }
+
+        public virtual void QueueDraw ()
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                contents.Widget.QueueDraw ();
+            });
+        }
+
+        public virtual void Refresh ()
+        {
+            ThreadAssist.ProxyToMain (delegate {
+               if (Context != null) {
+                    Context.Reset ();
+                    GetChannelsAsync (Context);
+               } else {
+                    GetChannelsAsync ();
+               }
+            });
+        }
+
+        protected virtual void GetChannelsAsync ()
+        {
+        }
+
+        protected virtual void GetChannelsAsync (string filterValue)
+        {
+            Client.GetChannelsAsync (
+                filter_type, filterValue, active_sort_type,
+                !(active_sort_type == MiroGuideSortType.Name), 50, 0, this
+            );
+        }
+
+        protected virtual void GetChannelsAsync (SearchContext context)
+        {
+            if (context != null) {
+                Client.GetChannelsAsync (context, this);
+            }
+        }
+
+        protected virtual void CheckVScrollbarValue (VScrollbar vsb)
+        {
+            if (!ignore_scroll && (vsb.Value == vsb.Adjustment.Upper-vsb.Adjustment.PageSize ||
+                vsb.Adjustment.Upper-vsb.Adjustment.PageSize < 0)) {
+                if (Context != null && Context.ChannelsAvailable) {
+                    GetChannelsAsync (Context);
+                }
+            }
+        }
+
+        protected virtual void ClearModel ()
+        {
+            ChannelModel.Selection.Clear ();
+            ChannelModel.Clear ();
+        }
+
+        protected virtual ChannelSourceContents CreateChannelSourceContents ()
+        {
+            return new ChannelSourceContents ();
+        }
+
+        protected virtual void RefreshArtworkFor (MiroGuideChannelInfo channel)
+        {
+            if (!CoverArtSpec.CoverExists (PaasService.ArtworkIdFor (channel.Name))) {
+                Banshee.Kernel.Scheduler.Schedule (
+                    new MiroGuideImageFetchJob (channel), Banshee.Kernel.JobPriority.AboveNormal
+                );
+            }
+        }
+
+        protected SourceMessage CreateStatusMessage (string messageStr, string iconName)
+        {
+            return CreateStatusMessage (messageStr, iconName, null, null);
+        }
+
+        protected SourceMessage CreateStatusMessage (string messageStr, string iconName, string actionStr, EventHandler action)
+        {
+            SourceMessage message = new SourceMessage (this) {
+                Text = messageStr
+            };
+
+            message.SetIconName (iconName);
+
+            if (action != null) {
+                message.AddAction (new MessageAction (actionStr, false, action));
+            }
+
+            return message;
+        }
+
+        protected void SetStatus (string messageStr)
+        {
+            SetStatus (CreateStatusMessage (messageStr, Stock.Info));
+        }
+
+        protected void SetStatus (SourceMessage message)
+        {
+            ThreadAssist.AssertInMainThread ();
+            status_message = message;
+        }
+
+        protected void SetErrorStatus ()
+        {
+            SetErrorStatus (Catalog.GetString ("An error occurred while communicating with Miro Guide."));
+        }
+
+        protected void SetErrorStatus (string message_str)
+        {
+            SetErrorStatus (CreateStatusMessage (
+                message_str, Stock.DialogError, Catalog.GetString ("Retry"), delegate { Refresh (); }
+            ));
+        }
+
+        protected void SetErrorStatus (SourceMessage message)
+        {
+            ThreadAssist.AssertInMainThread ();
+            error_status_message = message;
+        }
+
+        // Terrible name.
+        protected void SetFetchStatus ()
+        {
+            ThreadAssist.AssertInMainThread ();
+
+            SetStatus (CreateStatusMessage (
+                Catalog.GetString ("Fetch additional channels?"), Stock.DialogQuestion,
+                Catalog.GetString ("Fetch"), delegate { GetChannelsAsync (Context); }
+            ));
+        }
+
+        protected virtual void SetRequestStatus (string message)
+        {
+            SetRequestStatus (message, null);
+        }
+
+        protected virtual void SetRequestStatus (string message, string iconName)
+        {
+            ThreadAssist.AssertInMainThread ();
+
+            SourceMessage m = new SourceMessage (this) {
+                Text = message,
+                CanClose = true,
+                IsSpinning = true
+            };
+
+            m.Updated += (sender, e) => {
+                if (m.IsHidden) {
+                    Client.CancelAsync ();
+                }
+            };
+
+            PushMessage (m);
+        }
+
+        protected virtual void OnMiroGuideClientStateChanged (object sender, AetherClientStateChangedEventArgs e)
+        {
+            if (e.NewState == AetherClientState.Busy) {
+                ClientHandle.Reset ();
+                ThreadAssist.ProxyToMain (delegate {
+                    ClearMessages ();
+                    status_message = null;
+                    error_status_message = null;
+                    actions["MiroGuideRefreshChannelsAction"].Sensitive = false;
+                    ChannelSourceContents.SortPreferenceButtonSensitive = false;
+                    SetRequestStatus (String.Format ("{0}...", BusyStatusMessage));
+                });
+            } else {
+                ClientHandle.Set ();
+                ThreadAssist.ProxyToMain (delegate {
+                    actions["MiroGuideRefreshChannelsAction"].Sensitive = true;
+                    ChannelSourceContents.SortPreferenceButtonSensitive = true;
+                    PopMessage ();
+
+                    if (status_message != null) {
+                        PushMessage (status_message);
+                    } else if (error_status_message != null) {
+                        PushMessage (error_status_message);
+                    }
+                });
+            }
+        }
+
+        protected virtual void OnVScrollbarValueChangedHandler (object sender, EventArgs e)
+        {
+            CheckVScrollbarValue (sender as VScrollbar);
+        }
+
+        protected virtual void OnGetChannelsCompletedHandler (object sender, GetChannelsCompletedEventArgs e)
+        {
+            if (e.UserState != this) {
+                return;
+            }
+
+            ThreadAssist.ProxyToMain (delegate {
+                if (e.Cancelled) {
+                    if (Context != null && Context.ChannelsAvailable) {
+                        SetFetchStatus ();
+                    }
+                    return;
+                } else if (e.Error != null) {
+                    SetErrorStatus ();
+                    return;
+                }
+
+                Context = e.Context;
+
+                ignore_scroll = true;
+                bool check_sb_pos = false;
+
+                foreach (MiroGuideChannelInfo channel in e.Channels.Reverse ()) {
+                    RefreshArtworkFor (channel);
+                }
+
+                if (Context.Page == 0) {
+                    ClearModel ();
+                }
+
+                if (Context.Count > 0) {
+                    ChannelModel.Add (e.Channels);
+
+                    if (Context.ChannelsAvailable) {
+                        Contents.ScrolledWindow.VscrollbarPolicy = PolicyType.Always;
+                        check_sb_pos = true;
+                    }
+                } else {
+                    if (Context.Page == 0) {
+                        SetStatus (Catalog.GetString ("No matches found."));
+                    }
+
+                    Contents.ScrolledWindow.VscrollbarPolicy = PolicyType.Automatic;
+                    ChannelModel.Reload ();
+                }
+
+                ignore_scroll = false;
+
+                if (check_sb_pos) {
+                    SetFetchStatus ();
+                    CheckVScrollbarValue (Contents.ScrolledWindow.VScrollbar as VScrollbar);
+                }
+            });
+        }
+
+        protected virtual void OnSortPreferenceChangedHandler (object sender, SortPreferenceChangedEventArgs e)
+        {
+            if (e.ActiveSource != this) {
+                return;
+            }
+
+            ThreadAssist.ProxyToMain (delegate {
+                if (Context != null && Context.SortType != e.Sort) {
+                    Context.Reset ();
+                    Context.SortType = e.Sort;
+                    active_sort_type = e.Sort;
+
+                    Context.Reverse = !(active_sort_type == MiroGuideSortType.Name);
+
+                    ClearModel ();
+                    GetChannelsAsync (Context);
+                } else {
+                    GetChannelsAsync ();
+                }
+            });
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/FeaturedChannelsSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/FeaturedChannelsSource.cs
new file mode 100644
index 0000000..2418e24
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/FeaturedChannelsSource.cs
@@ -0,0 +1,61 @@
+//
+// FeaturedChannelsSource.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Mono.Unix;
+using Banshee.Paas.MiroGuide.Gui;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class FeaturedChannelsSource : ChannelSource
+    {
+        public FeaturedChannelsSource (MiroGuideClient client) : base (client,
+                                                                       MiroGuideFilterType.Featured,
+                                                                       "MiroGuideFeaturedChannels",
+                                                                       Catalog.GetString ("Featured"),
+                                                                       (int)MiroGuideSourcePosition.Featured)
+        {
+            Properties.SetStringList ("Icon.Name", "emblem-favorite");
+            Properties.Set<bool> ("MiroGuide.Gui.Source.ShowSortPreference", true);
+            BusyStatusMessage = Catalog.GetString ("Receiving Featured Channels from Miro Guide");
+        }
+
+        public override void Activate ()
+        {
+            base.Activate ();
+
+            if (Context == null) {
+                GetChannelsAsync ();
+            }
+        }
+
+        protected override void GetChannelsAsync ()
+        {
+            GetChannelsAsync ("1");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/HDChannelsSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/HDChannelsSource.cs
new file mode 100644
index 0000000..d45d329
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/HDChannelsSource.cs
@@ -0,0 +1,62 @@
+//
+// HDChannelsSource.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Mono.Unix;
+using Banshee.Paas.MiroGuide.Gui;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class HDChannelsSource : ChannelSource
+    {
+        public HDChannelsSource (MiroGuideClient client) : base (client,
+                                                                 MiroGuideFilterType.HD,
+                                                                 "MiroGuideHDChannels",
+                                                                 Catalog.GetString ("HD Channels"),
+                                                                 (int)MiroGuideSourcePosition.HD)
+        {
+            ActiveSortType = MiroGuideSortType.Rating;
+            Properties.SetStringList ("Icon.Name", "video-x-generic");
+            Properties.Set<bool> ("MiroGuide.Gui.Source.ShowSortPreference", true);
+            BusyStatusMessage = Catalog.GetString ("Receiving HD Channels from Miro Guide");
+        }
+
+        public override void Activate ()
+        {
+            base.Activate ();
+
+            if (Context == null) {
+                GetChannelsAsync ();
+            }
+        }
+
+        protected override void GetChannelsAsync ()
+        {
+            GetChannelsAsync ("1");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/MiroGuideSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/MiroGuideSource.cs
new file mode 100644
index 0000000..5753dfd
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/MiroGuideSource.cs
@@ -0,0 +1,50 @@
+//
+// MiroGuideSource.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Banshee.Sources;
+using Banshee.Sources.Gui;
+
+using Banshee.Paas.MiroGuide.Gui;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class MiroGuideSource : Source
+    {
+        public MiroGuideSource (MiroGuideClient client) : base ("Miro Guide", "Miro Guide", 201)
+        {
+            Properties.SetStringList ("Icon.Name", "miro");
+            Properties.SetString ("GtkActionPath", "/MiroGuideSourcePopup");
+
+            Properties.Set<ISourceContents> ("Nereid.SourceContents", new MiroGuideSourceContents ());
+            Properties.Set<bool> ("Nereid.SourceContentsPropagate", false);
+
+            Properties.Set<bool> ("Nereid.SourceContents.HeaderVisible", false);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/MiroGuideSourcePosition.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/MiroGuideSourcePosition.cs
new file mode 100644
index 0000000..6c7e484
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/MiroGuideSourcePosition.cs
@@ -0,0 +1,41 @@
+//
+// MiroGuideSourcePosition.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public enum MiroGuideSourcePosition : int
+    {
+        Search = 0,
+        Browse,
+        Featured,
+        TopRated,
+        Popular,
+        Recommended,
+        HD
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/PopularChannelsSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/PopularChannelsSource.cs
new file mode 100644
index 0000000..fef8532
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/PopularChannelsSource.cs
@@ -0,0 +1,61 @@
+//
+// PopularChannelsSource.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Mono.Unix;
+using Banshee.Paas.MiroGuide.Gui;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class PopularChannelsSource : ChannelSource
+    {
+        public PopularChannelsSource (MiroGuideClient client) : base (client,
+                                                                      MiroGuideFilterType.Popular,
+                                                                      "MiroGuidePopularChannels",
+                                                                      Catalog.GetString ("Most Popular"),
+                                                                      (int)MiroGuideSourcePosition.Popular)
+        {
+            ActiveSortType = MiroGuideSortType.Popular;
+            Properties.SetStringList ("Icon.Name", "system-users");
+            BusyStatusMessage = Catalog.GetString ("Receiving Popular Channels from Miro Guide");
+        }
+
+        public override void Activate ()
+        {
+            base.Activate ();
+
+            if (Context == null) {
+                GetChannelsAsync ();
+            }
+        }
+
+        protected override void GetChannelsAsync ()
+        {
+            GetChannelsAsync ("1");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/RecommendedChannelsSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/RecommendedChannelsSource.cs
new file mode 100644
index 0000000..7625d8c
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/RecommendedChannelsSource.cs
@@ -0,0 +1,46 @@
+//
+// RecommendedChannelsSource.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Mono.Unix;
+using Banshee.Paas.MiroGuide.Gui;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class RecommendedChannelsSource : ChannelSource
+    {
+        public RecommendedChannelsSource (MiroGuideClient client) : base (client,
+                                                                          MiroGuideFilterType.Featured,
+                                                                          "MiroGuideRecommendedChannels",
+                                                                          Catalog.GetString ("Recommended"),
+                                                                          (int)MiroGuideSourcePosition.Recommended)
+        {
+            Properties.SetStringList ("Icon.Name", "recommended");
+            Properties.Set<bool> ("MiroGuide.Gui.Source.ShowSortPreference", true);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/SearchSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/SearchSource.cs
new file mode 100644
index 0000000..a606f7a
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/SearchSource.cs
@@ -0,0 +1,156 @@
+//
+// SearchSource.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Mono.Unix;
+
+using Gtk;
+
+using Banshee.Base;
+using Banshee.Widgets;
+
+using Banshee.Sources.Gui;
+
+using Banshee.Paas.Aether;
+using Banshee.Paas.Aether.MiroGuide;
+
+using Banshee.Paas.MiroGuide.Gui;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class SearchSource : ChannelSource, IImplementsCustomSearch
+    {
+        private MiroGuideSearchEntry search_entry;
+
+        public SearchEntry SearchEntry
+        {
+            get { return search_entry; }
+        }
+
+        public SearchSource (MiroGuideClient client) : base (client,
+                                                             MiroGuideFilterType.Search,
+                                                             "MiroGuideSearch",
+                                                             Catalog.GetString ("Search"),
+                                                             (int)MiroGuideSourcePosition.Search)
+        {
+            BuildSearchEntry ();
+
+            ActiveSortType = MiroGuideSortType.Relevance;
+
+            BusyStatusMessage = "Searching Miro Guide";
+            Properties.SetStringList ("Icon.Name", "find");
+            Properties.Set<bool> ("MiroGuide.Gui.Source.ShowSortPreference", true);
+        }
+
+        public override void Activate ()
+        {
+            base.Activate ();
+            Actions["MiroGuideRefreshChannelsAction"].Visible = false;
+            Actions["MiroGuideSortByRelevanceAction"].Visible = true;
+        }
+
+        public override void Deactivate ()
+        {
+            base.Deactivate ();
+            Actions["MiroGuideRefreshChannelsAction"].Visible = true;
+            Actions["MiroGuideSortByRelevanceAction"].Visible = false;
+        }
+
+        protected override void GetChannelsAsync ()
+        {
+            string search = GetSearchString ();
+
+            if (!String.IsNullOrEmpty (search)) {
+                GetChannelsAsync (search);
+            }
+        }
+
+        private void BuildSearchEntry ()
+        {
+            search_entry = new MiroGuideSearchEntry ();
+            search_entry.SetSizeRequest (200, -1);
+
+            search_entry.Activated += OnSearchEntryActivated;
+            search_entry.Changed += OnSearchEntryChanged;
+
+            search_entry.Cleared += (sender, e) => { Reset (false); };
+            search_entry.FilterChanged += (sender, e) => { Reset (true); };
+
+            search_entry.Show ();
+        }
+
+        private string GetSearchString ()
+        {
+            string ret = search_entry.InnerEntry.Text.Trim ();
+
+            switch ((MiroGuideSearchFilter)search_entry.ActiveFilterID) {
+            case MiroGuideSearchFilter.HD:    ret += " hd";    break;
+            case MiroGuideSearchFilter.Audio: ret += " audio"; break;
+            case MiroGuideSearchFilter.Video: ret += " video"; break;
+            }
+
+            return ret;
+        }
+
+        private void Reset (bool refresh)
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                Context = null;
+                Contents.ScrolledWindow.VscrollbarPolicy = PolicyType.Automatic;
+
+                ClearMessages ();
+
+                ChannelModel.Selection.Clear ();
+                ChannelModel.Clear ();
+
+                if (refresh) {
+                    Refresh ();
+                }
+            });
+        }
+
+        protected override void OnMiroGuideClientStateChanged (object sender, AetherClientStateChangedEventArgs e)
+        {
+            ThreadAssist.ProxyToMain (delegate {
+                search_entry.Ready = (e.NewState == AetherClientState.Idle);
+                search_entry.Sensitive = (e.NewState == AetherClientState.Idle);
+            });
+
+            base.OnMiroGuideClientStateChanged (sender, e);
+        }
+
+        private void OnSearchEntryActivated (object sender, EventArgs e)
+        {
+            GetChannelsAsync ();
+        }
+
+        private void OnSearchEntryChanged (object sender, EventArgs e)
+        {
+            FilterQuery = search_entry.InnerEntry.Text;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/TopRatedChannelsSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/TopRatedChannelsSource.cs
new file mode 100644
index 0000000..c6509c8
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.MiroGuide/Sources/TopRatedChannelsSource.cs
@@ -0,0 +1,61 @@
+//
+// TopRatedChannelsSource.cs
+//
+// Author:
+//   Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Mono.Unix;
+using Banshee.Paas.MiroGuide.Gui;
+using Banshee.Paas.Aether.MiroGuide;
+
+namespace Banshee.Paas.MiroGuide
+{
+    public class TopRatedChannelsSource : ChannelSource
+    {
+        public TopRatedChannelsSource (MiroGuideClient client) : base (client,
+                                                                       MiroGuideFilterType.TopRated,
+                                                                       "MiroGuideTopRatedChannels",
+                                                                       Catalog.GetString ("Top Rated"),
+                                                                       (int)MiroGuideSourcePosition.TopRated)
+        {
+            ActiveSortType = MiroGuideSortType.Rating;
+            Properties.SetStringList ("Icon.Name", "system-users");
+            BusyStatusMessage = Catalog.GetString ("Receiving Top Rated Channels from Miro Guide");
+        }
+
+        public override void Activate ()
+        {
+            base.Activate ();
+
+            if (Context == null) {
+                GetChannelsAsync ();
+            }
+        }
+
+        protected override void GetChannelsAsync ()
+        {
+            GetChannelsAsync ("1");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Utils/OpmlParser.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Utils/OpmlParser.cs
new file mode 100644
index 0000000..d123114
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Utils/OpmlParser.cs
@@ -0,0 +1,93 @@
+//
+// OpmlParser.cs
+//
+// Authors:
+//   Gabriel Burt <gburt novell com>
+//   Brandan Lloyd
+//
+// Copyright (C) 2008 Novell, Inc.
+// Copyright (C) 2008 Brandan Lloyd
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Xml;
+using System.Collections.Generic;
+
+namespace Banshee.Paas.Utils
+{
+    public class OpmlParser
+    {
+        private XmlDocument doc;
+        private System.Collections.Generic.List<string> feeds = null;
+
+        public OpmlParser (string xml, bool fromFile)
+        {
+            doc = new XmlDocument ();
+            try {
+                if (fromFile) {
+                    doc.Load (xml);
+                } else {
+                    doc.LoadXml (xml);
+                }
+            } catch (XmlException) {
+                throw new FormatException ("Invalid xml document.");
+            }
+            VerifyOpml ();
+        }
+
+        public IEnumerable<string> Feeds {
+            get {
+                if (null == feeds) {
+                    feeds = new List<string> ();
+                    GetFeeds (doc.SelectSingleNode ("/opml/body"));
+                }
+                return feeds;
+            }
+        }
+
+        private void GetFeeds (XmlNode baseNode)
+        {
+            XmlNodeList nodes = baseNode.SelectNodes ("./outline");
+            foreach (XmlNode node in nodes) {
+                if (node.Attributes["xmlUrl"] != null) {
+                    feeds.Add (node.Attributes["xmlUrl"].Value);
+                } else if (node.Attributes["url"] != null) {
+                    feeds.Add (node.Attributes["url"].Value);
+                }
+
+                GetFeeds (node);
+            }
+        }
+
+        private void VerifyOpml ()
+        {
+            if (doc.SelectSingleNode ("/opml") == null)
+                throw new FormatException ("Invalid OPML document.");
+
+            if (doc.SelectSingleNode ("/opml/head") == null)
+                throw new FormatException ("Invalid OPML document.");
+
+            if (doc.SelectSingleNode ("/opml/body") == null)
+                throw new FormatException ("Invalid OPML document.");
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Utils/StringUtils.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Utils/StringUtils.cs
new file mode 100644
index 0000000..9bb9fbc
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas.Utils/StringUtils.cs
@@ -0,0 +1,54 @@
+//
+// StringUtils.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Web;
+
+namespace Banshee.Paas.Utils
+{
+    public static class StringUtils
+    {
+        public static string StripHtml (string str)
+        {
+            str = Hyena.StringUtil.RemoveHtml (str);
+
+            if (str != null) {
+                str = HttpUtility.HtmlDecode (str);
+            }
+
+            return str;
+        }
+
+        public static string DecodeUrl (string str)
+        {
+            if (str != null) {
+                str = Uri.UnescapeDataString (str);
+            }
+
+            return str;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas/PaasImageFetchJob.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas/PaasImageFetchJob.cs
new file mode 100644
index 0000000..d6ec9a4
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas/PaasImageFetchJob.cs
@@ -0,0 +1,83 @@
+//
+// PaasImageFetchJob.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+
+using System;
+
+using Hyena;
+
+using Banshee.Base;
+using Banshee.Metadata;
+using Banshee.ServiceStack;
+
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas
+{
+    public class PaasImageFetchJob : MetadataServiceJob
+    {
+        private PaasChannel channel;
+
+        public PaasImageFetchJob (PaasChannel channel) : base ()
+        {
+            this.channel = channel;
+        }
+
+        public override void Run()
+        {
+            Fetch ();
+        }
+
+        public void Fetch ()
+        {
+            if (channel.ImageUrl == null) {
+                return;
+            }
+
+            string cover_art_id = PaasService.ArtworkIdFor (channel);
+
+            if (cover_art_id == null) {
+                return;
+            } else if (CoverArtSpec.CoverExists (cover_art_id)) {
+                return;
+            } else if (!InternetConnected) {
+                return;
+            }
+
+            if (SaveHttpStreamCover (new Uri (channel.ImageUrl), cover_art_id, null)) {
+                Banshee.Sources.Source src = ServiceManager.SourceManager.ActiveSource;
+
+                if (src != null && (src is PaasSource || src.Parent is PaasSource)) {
+                    (src as PaasSource).QueueDraw ();
+                }
+
+                return;
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas/PaasService.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas/PaasService.cs
new file mode 100644
index 0000000..a47dcd4
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas/PaasService.cs
@@ -0,0 +1,833 @@
+//
+// PaasService.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.Net;
+
+using System.Linq;
+using System.Xml.Linq;
+
+using System.Threading;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Hyena;
+
+using Banshee.IO;
+using Banshee.Web;
+using Banshee.Base;
+using Banshee.Sources;
+using Banshee.Networking;
+using Banshee.MediaEngine;
+using Banshee.ServiceStack;
+using Banshee.Configuration;
+
+using Banshee.Collection;
+using Banshee.Collection.Database;
+
+using Banshee.Paas.Gui;
+using Banshee.Paas.Data;
+using Banshee.Paas.Utils;
+
+using Banshee.Paas.MiroGuide;
+using Banshee.Paas.MiroGuide.Gui;
+
+using Banshee.Paas.Aether;
+using Banshee.Paas.Aether.MiroGuide;
+
+using Banshee.Paas.Aether.Syndication;
+
+using Banshee.Paas.DownloadManager;
+using Banshee.Paas.DownloadManager.Gui;
+
+using Migo2.Async;
+using Migo2.DownloadService;
+
+namespace Banshee.Paas
+{
+    delegate void Updater ();
+
+    public partial class PaasService : IExtensionService, IDisposable, IDelayedInitializeService
+    {
+        private int update_count;
+        private bool disposing, disposed;
+
+        private uint refresh_timeout_id = 0;
+
+        private readonly string tmp_download_path = Paths.Combine (
+            Paths.ExtensionCacheRoot, "paas", "partial-downloads"
+        );
+
+        private PaasSource source;
+
+        private MiroGuideClient mg_client;
+        private MiroGuideInterfaceManager mg_interface_manager;
+        public static MiroGuideAccountInfo MiroGuideAccount;
+
+        private SyndicationClient syndication_client;
+
+        private AutoResetEvent client_handle;
+
+        private PaasDownloadManager download_manager;
+        private DownloadManagerInterface download_manager_interface;
+
+        private Updater redraw, reload;
+
+        private readonly object sync = new object ();
+
+        // There needs to be a DownloadManager service in the service manager.
+        // There are issues adding a service to the service manager from an extension...
+        // It shouldn't be in the extension anyway.
+        public PaasDownloadManager DownloadManager {
+            get { return download_manager; }
+        }
+
+        public string ServiceName {
+            get { return "PaasService"; }
+        }
+
+        public PaasSource Source {
+            get { return source; }
+        }
+
+        public MiroGuideClient MiroGuideClient {
+            get { return mg_client; }
+        }
+
+        public SyndicationClient SyndicationClient {
+            get { return syndication_client; }
+        }
+
+        private bool Disposed {
+            get {
+                return (disposed | disposing);
+            }
+        }
+
+        static PaasService ()
+        {
+            MiroGuideAccount = new MiroGuideAccountInfo (
+                MiroGuideServiceUri.Get (), MiroGuideSessionID.Get (),
+                MiroGuideUsername.Get (), MiroGuidePasswordHash.Get ()
+            );
+
+            MiroGuideAccount.Updated += (sender, e) => {
+                MiroGuideUsername.Set (MiroGuideAccount.Username);
+                MiroGuidePasswordHash.Set (MiroGuideAccount.PasswordHash);
+                MiroGuideSessionID.Set (MiroGuideAccount.SessionID);
+                MiroGuideServiceUri.Set (MiroGuideAccount.ServiceUri);
+            };
+
+//            https://bugzilla.novell.com/show_bug.cgi?id=346561
+//            ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => {
+//                return true;
+//            };
+        }
+
+        public PaasService ()
+        {
+            reload = () => {
+                if (!Disposed) {
+                    source.Reload ();
+                }
+            };
+
+            redraw = () => {
+                if (!Disposed) {
+                    source.QueueDraw ();
+                }
+            };
+
+            source = new PaasSource (this);
+
+            if (source.DbId < 1) {
+                source.Save ();
+            }
+
+            client_handle = new AutoResetEvent (true);
+
+            mg_client = new MiroGuideClient (MiroGuideAccount) {
+                UserAgent = Banshee.Web.Browser.UserAgent,
+            };
+
+            mg_client.StateChanged += (sender, e) => {
+                if (e.NewState == AetherClientState.Idle) {
+                    DecrementUpdateCount ();
+                    client_handle.Set ();
+                } else {
+                    IncrementUpdateCount ();
+                    client_handle.Reset ();
+                }
+            };
+
+            mg_client.SubscriptionRequested += (sender, e) => {
+                if (e.Uri != null) {
+                    SubscribeToChannel (e.Uri);
+                } else if (e.Uris != null) {
+                    SubscribeToChannels (e.Uris);
+                }
+            };
+
+            syndication_client = new SyndicationClient (source);
+
+            syndication_client.StateChanged += (sender, e) => {
+                if (e.NewState == AetherClientState.Idle) {
+                    DecrementUpdateCount ();
+                } else {
+                    IncrementUpdateCount ();
+                }
+            };
+
+            syndication_client.ChannelsAdded += (sender, e) => {
+                lock (sync) {
+                    if (Disposed) {
+                        return;
+                    }
+
+                    syndication_client.QueueUpdate (e.Channel);
+                    reload ();
+                }
+            };
+
+            syndication_client.ChannelsRemoved += (sender, e) => { reload (); };
+            syndication_client.ChannelUpdating += (sender, e) => { redraw (); };
+            syndication_client.ChannelUpdateCompleted += OnChannelUpdatedHandler;
+
+            syndication_client.ItemsAdded += OnItemsAddedHandler;
+            syndication_client.ItemsRemoved += OnItemsRemovedHandler;
+
+            download_manager = new PaasDownloadManager (source.DbId, 2, tmp_download_path);
+            download_manager.TaskAdded += (sender, e) => { redraw (); };
+
+            download_manager.TaskCompleted += OnDownloadTaskCompletedHandler;
+            download_manager.TaskStateChanged += (sender, e) => { redraw ();; };
+        }
+
+        public void UpdateAsync ()
+        {
+            lock (sync) {
+                if (!Disposed) {
+                    syndication_client.UpdateAsync ();
+                }
+            }
+        }
+
+        public void Initialize ()
+        {
+            InitializeInterface ();
+        }
+
+        public void DelayedInitialize ()
+        {
+            ServiceManager.Get<DBusCommandService> ().ArgumentPushed += OnCommandLineArgument;
+
+            //download_manager.RestoreQueuedDownloads ();
+            RefreshFeeds (true);
+            refresh_timeout_id = Application.RunTimeout (1000 * 60 * 10, RefreshFeeds);  // 10 minutes
+        }
+
+        public void Dispose ()
+        {
+            lock (sync) {
+                if (Disposed) {
+                    return;
+                }
+
+                disposing = true;
+            }
+
+            Application.IdleTimeoutRemove (refresh_timeout_id);
+            refresh_timeout_id = 0;
+
+            DisposeInterface ();
+            ServiceManager.Get<DBusCommandService> ().ArgumentPushed -= OnCommandLineArgument;
+
+            mg_client.CancelAsync ();
+            client_handle.WaitOne ();
+
+            mg_client = null;
+
+            client_handle.Close ();
+            client_handle = null;
+
+            syndication_client.Dispose ();
+            syndication_client.ItemsAdded -= OnItemsAddedHandler;
+            syndication_client.ItemsRemoved -= OnItemsRemovedHandler;
+            syndication_client.ChannelUpdateCompleted -= OnChannelUpdatedHandler;
+
+            syndication_client = null;
+
+            download_manager.Dispose ();
+            download_manager.TaskCompleted -= OnDownloadTaskCompletedHandler;
+            download_manager = null;
+
+            lock (sync) {
+                disposing = false;
+                disposed  = true;
+            }
+        }
+
+        public void DeleteChannels (IEnumerable<PaasChannel> channels, bool deleteFiles)
+        {
+            lock (sync) {
+                if (Disposed) {
+                    return;
+                }
+
+                syndication_client.DeleteChannels (
+                    channels.Where (c => c.ClientID == (int)AetherClientID.Syndication), deleteFiles
+                );
+            }
+        }
+
+        public void ExportChannelsToOpml (string path, IEnumerable<PaasChannel> channels)
+        {
+            if (String.IsNullOrEmpty (path)) {
+                throw new ArgumentNullException ("path");
+            } else if (channels == null) {
+                throw new ArgumentNullException ("channels");
+            }
+
+            if (channels.Count () == 0) {
+                return;
+            }
+
+            try {
+                // Move this to a helper class
+                XDocument doc = new XDocument (
+                    new XDeclaration ("1.0", "utf-8", "yes"),
+                    new XElement ("opml",
+                        new XAttribute ("version", "2.0"),
+                        new XElement ("head",
+                                new XElement ("title", Catalog.GetString ("Banshee Podcast Subscriptions")),
+                                new XElement ("dateCreated", DateTime.UtcNow.ToString ("ddd, dd MMM yyyy HH:mm:ss K"))
+                        ), new XElement ("body",
+                            from channel in channels select new XElement (
+                                "outline",
+                                new XAttribute ("type", "rss"),
+                                new XAttribute ("text", channel.Name),
+                                new XAttribute ("xmlUrl", channel.Url)
+                            )
+                        )
+                    )
+                );
+
+                doc.Save (path);
+            } catch (Exception e) {
+                Log.Exception (e);
+                throw;
+            }
+        }
+
+        public void ImportOpml (string path)
+        {
+            if (String.IsNullOrEmpty (path)) {
+                throw new ArgumentNullException ("path");
+            }
+
+            lock (sync) {
+                if (Disposed) {
+                    return;
+                }
+
+                try {
+                    OpmlParser opml_parser = new OpmlParser (path, true);
+
+                    foreach (string channel in opml_parser.Feeds) {
+                        try {
+                            syndication_client.SubscribeToChannel (channel, DownloadPreference.One);
+                        } catch (Exception e) {
+                            Log.Exception (e);
+                        }
+                    }
+
+                    source.NotifyUser ();
+                } catch (Exception e) {
+                    Log.Exception (e);
+                    throw;
+                }
+            }
+        }
+
+        public void SubscribeToChannels (IEnumerable<Uri> uris)
+        {
+            if (uris == null) {
+                throw new ArgumentNullException ("uris");
+            }
+
+            lock (sync) {
+                if (Disposed) {
+                    return;
+                }
+
+                int sub_count = 0;
+
+                foreach (Uri uri in uris) {
+                    try {
+                        if (uri != null) {
+                            syndication_client.SubscribeToChannel (uri.ToString (), DownloadPreference.One);
+                            sub_count++;
+                        }
+                    } catch (Exception e) {
+                        Log.Exception (e);
+                        continue;
+                    }
+                }
+
+                if (sub_count > 0) {
+                    source.NotifyUser ();
+                }
+            }
+        }
+
+        public void SubscribeToChannel (Uri uri)
+        {
+            if (uri == null) {
+                throw new ArgumentNullException ("uri");
+            }
+
+            lock (sync) {
+                if (Disposed) {
+                    return;
+                }
+
+                try {
+                    syndication_client.SubscribeToChannel (uri.ToString (), DownloadPreference.One);
+                    source.NotifyUser ();
+                } catch (Exception e) {
+                    Log.Exception (e);
+                }
+            }
+        }
+
+        public void QueueDownload (PaasItem item)
+        {
+            if (item.Error) {
+                item.Error = false;
+                item.Save ();
+                redraw ();
+            }
+
+            download_manager.QueueDownload (item);
+        }
+
+        public void QueueDownload (IEnumerable<PaasItem> items)
+        {
+            ServiceManager.DbConnection.BeginTransaction ();
+
+            try {
+                bool redraw_requested = false;
+
+                foreach (var item in items.Where (i => i.Error)) {
+                    item.Error = false;
+                    item.Save ();
+                    redraw_requested = true;
+                }
+
+                ServiceManager.DbConnection.CommitTransaction ();
+
+                if (redraw_requested) {
+                    redraw ();
+                }
+            } catch {
+                ServiceManager.DbConnection.RollbackTransaction ();
+                throw;
+            }
+
+            download_manager.QueueDownload (items);
+        }
+
+        private void InitializeInterface ()
+        {
+            ServiceManager.SourceManager.AddSource (source);
+
+            mg_interface_manager = new MiroGuideInterfaceManager ();
+            mg_interface_manager.Initialize (mg_client);
+
+            download_manager_interface = new DownloadManagerInterface (source, download_manager);
+        }
+
+        private void DisposeInterface ()
+        {
+            if (source != null) {
+                ServiceManager.SourceManager.RemoveSource (source);
+                source.Dispose ();
+            }
+
+            if (mg_interface_manager != null) {
+                mg_interface_manager.Dispose ();
+                mg_interface_manager = null;
+            }
+
+            if (download_manager_interface != null) {
+                download_manager_interface.Dispose ();
+                download_manager_interface = null;
+            }
+        }
+
+        private DatabaseTrackInfo GetTrackByItemId (long item_id)
+        {
+           return DatabaseTrackInfo.Provider.FetchFirstMatching (
+                "PrimarySourceID = ? AND ExternalID = ?", source.DbId, item_id
+            );
+        }
+
+        private bool RefreshFeeds ()
+        {
+            return RefreshFeeds (false);
+        }
+
+        private bool RefreshFeeds (bool forceRefresh)
+        {
+            Hyena.Log.Debug ("Refreshing any podcasts that haven't been updated in over an hour");
+
+            Banshee.Kernel.Scheduler.Schedule (new Banshee.Kernel.DelegateJob (delegate {
+                try {
+                    DateTime now = DateTime.Now;
+
+                    syndication_client.QueueUpdate (
+                        PaasChannel.Provider.FetchAll ().Where (
+                            c => (now - c.LastDownloadTime).TotalHours > 1 || forceRefresh
+                        )
+                    );
+                } catch (Exception e) {
+                    Hyena.Log.Exception (e);
+                }
+            }));
+
+            return true;
+        }
+
+        private void OnChannelUpdatedHandler (object sender, ChannelUpdateCompletedEventArgs e)
+        {
+            lock (sync) {
+                if (Disposed) {
+                    return;
+                }
+
+                if (e.Succeeded) {
+                    PaasChannel channel = e.Channel;
+#if EDIT_DIR_TEST
+                    if (String.IsNullOrEmpty (channel.LocalEnclosurePath)) {
+                        string escaped = Hyena.StringUtil.EscapeFilename (channel.Name);
+
+                        channel.LocalEnclosurePath = Path.Combine (source.BaseDirectory, escaped);
+                        channel.Save ();
+
+                        try {
+                            Banshee.IO.Directory.Create (channel.LocalEnclosurePath);
+                        } catch (Exception ex) {
+                            Hyena.Log.Exception (ex);
+                        }
+                    }
+#endif
+                    IEnumerable<PaasItem> items = channel.Items.OrderByDescending (i => i.PubDate);
+
+                    switch (e.Channel.DownloadPreference) {
+                    case DownloadPreference.One:
+                        items = items.Take (1);
+                        break;
+                    case DownloadPreference.None:
+                        items = items.Take (0);
+                        break;
+                    }
+
+                    RefreshArtworkFor (channel);
+                    QueueDownload (items.Where (i => i.Active && !i.IsDownloaded));
+                } else if (e.Error != null) {
+                    Log.Exception (e.Error);
+
+                    source.ErrorSource.AddMessage (
+                        String.Format (Catalog.GetString (@"Error while updating channel ""{0}"""), e.Channel.Name),
+                        e.Error.Message
+                    );
+                }
+
+                redraw ();
+                //reload ();
+            }
+        }
+
+        private void OnItemsAddedHandler (object sender, ItemEventArgs e)
+        {
+            reload ();
+        }
+
+        private void OnItemsRemovedHandler (object sender, ItemEventArgs e)
+        {
+            lock (sync) {
+                if (Disposed) {
+                    return;
+                }
+
+                if (e.Item != null) {
+                    if (download_manager.Contains (e.Item)) {
+                        download_manager.CancelDownload (e.Item);
+                    }
+                } else {
+                    foreach (PaasItem item in e.Items) {
+                        if (download_manager.Contains (item)) {
+                            download_manager.CancelDownload (item);
+                        }
+                    }
+                }
+
+                reload ();
+            }
+        }
+
+        private void OnDownloadTaskCompletedHandler (object sender, TaskCompletedEventArgs<HttpFileDownloadTask> e)
+        {
+            PaasItem item = e.UserState as PaasItem;
+
+            if (item == null) {
+                return;
+            }
+
+            lock (sync) {
+                if (Disposed) {
+                    return;
+                } else if (e.State != TaskState.Succeeded) {
+                    if (e.Error != null) {
+                        source.ErrorSource.AddMessage (
+                            String.Format (
+                                Catalog.GetString ("Error Downloading:  {0}"), (e.Task as HttpFileDownloadTask).Name
+                            ), e.Error.Message
+                        );
+
+                        if (ServiceManager.Get<Network> ().Connected) {
+                            item.Error = true;
+                            item.Save ();
+                            redraw ();
+                        }
+                    }
+
+                    return;
+                }
+
+                string path = Path.GetDirectoryName (e.Task.LocalPath);
+                string filename = Path.GetFileName (e.Task.LocalPath);
+                string full_path = path;
+                string tmp_local_path;
+
+#if EDIT_DIR_TEST
+                string local_enclosure_path = item.Channel.LocalEnclosurePath;
+#else
+
+                string escaped = Hyena.StringUtil.EscapeFilename (item.Channel.Name);
+                string local_enclosure_path = Path.Combine (source.BaseDirectory, escaped);
+#endif
+
+                if (!local_enclosure_path.EndsWith (Path.DirectorySeparatorChar.ToString ())) {
+                    local_enclosure_path += Path.DirectorySeparatorChar;
+                }
+
+                if (!full_path.EndsWith (Path.DirectorySeparatorChar.ToString ())) {
+                    full_path += Path.DirectorySeparatorChar;
+                }
+
+                full_path += filename;
+                tmp_local_path = local_enclosure_path+StringUtils.DecodeUrl (filename);
+
+                try {
+                    // This is crap, non-i18n-iz-iz-ized strings will be displayed to the users.
+                    if (!Banshee.IO.Directory.Exists (path)) {
+                        throw new InvalidOperationException ("Directory specified by path does not exist");
+                    } else if (!Banshee.IO.File.Exists (new SafeUri (full_path))) {
+                        throw new InvalidOperationException (
+                            String.Format ("File:  {0}, does not exist", full_path)
+                        );
+                    }
+
+                    if (!Banshee.IO.Directory.Exists (local_enclosure_path)) {
+                        Banshee.IO.Directory.Create (local_enclosure_path);
+                    }
+
+                    if (Banshee.IO.File.Exists (new SafeUri (tmp_local_path))) {
+                        int last_dot = tmp_local_path.LastIndexOf (".");
+
+                        if (last_dot == -1) {
+                            last_dot = tmp_local_path.Length-1;
+                        }
+
+                        string rep = String.Format (
+                            "-{0}",
+                            Guid.NewGuid ().ToString ()
+                                           .Replace ("-", String.Empty)
+                                           .ToLower ()
+                        );
+
+                        tmp_local_path = tmp_local_path.Insert (last_dot, rep);
+                    }
+
+                    Banshee.IO.File.Move (new SafeUri (full_path), new SafeUri (tmp_local_path));
+
+                    try {
+                        Banshee.IO.Directory.Delete (path, true);
+                    } catch {}
+
+                    item.LocalPath = tmp_local_path;
+                    item.MimeType = e.Task.MimeType;
+                    item.DownloadedAt = DateTime.Now;
+
+                    DatabaseTrackInfo track = GetTrackByItemId (item.DbId);
+
+                    if (track != null) {
+                        new PaasTrackInfo (track, item).Track.Save ();
+                        item.IsNew = (track.PlayCount < 1);
+                        item.Save ();
+                    }
+
+                    source.NotifyUser ();
+                } catch (Exception ex) {
+                    source.ErrorSource.AddMessage (
+                        String.Format (Catalog.GetString ("Error Saving File:  {0}"), tmp_local_path), ex.Message
+                    );
+
+                    item.Error = true;
+                    item.Save ();
+
+                    Hyena.Log.Exception (ex);
+                    Hyena.Log.Error (ex.StackTrace);
+                }
+
+                reload ();
+            }
+        }
+
+        private void OnCommandLineArgument (string uri, object value, bool isFile)
+        {
+            if (!isFile || String.IsNullOrEmpty (uri)) {
+                return;
+            }
+
+            lock (sync) {
+                if (Disposed) {
+                    return;
+                }
+
+                if (uri.Contains ("opml") || uri.EndsWith (".miro") || uri.EndsWith (".democracy")) {
+                    try {
+                        OpmlParser opml_parser = new OpmlParser (uri, true);
+
+                        foreach (string channel in opml_parser.Feeds) {
+                            ServiceManager.Get<DBusCommandService> ().PushFile (channel);
+                        }
+                    } catch (Exception e) {
+                        Log.Exception (e);
+                    }
+                } else if (uri.Contains ("xml") || uri.Contains ("rss") || uri.Contains ("feed") || uri.StartsWith ("itpc") || uri.StartsWith ("pcast")) {
+                    if (uri.StartsWith ("feed://") || uri.StartsWith ("itpc://")) {
+                        uri = String.Format ("http://{0}";, uri.Substring (7));
+                    } else if (uri.StartsWith ("pcast://")) {
+                        uri = String.Format ("http://{0}";, uri.Substring (8));
+                    }
+
+                    syndication_client.SubscribeToChannel (uri, DownloadPreference.One);
+                    source.NotifyUser ();
+                } else if (uri.StartsWith ("itms://")) {
+                    System.Threading.ThreadPool.QueueUserWorkItem (delegate {
+                        try {
+                            string feed_url = new ItmsPodcast (uri).FeedUrl;
+
+                            if (feed_url != null) {
+                                ThreadAssist.ProxyToMain (delegate {
+                                    syndication_client.SubscribeToChannel (feed_url, DownloadPreference.None);
+                                    source.NotifyUser ();
+                                });
+                            }
+                        } catch (Exception e) {
+                            Hyena.Log.Exception (e);
+                        }
+                    });
+                }
+            }
+        }
+
+        private void IncrementUpdateCount ()
+        {
+            lock (sync) {
+                if (!Disposed) {
+                   if (update_count++ == 0) {
+                        ThreadAssist.ProxyToMain (delegate {
+                            source.SetStatus ("Updating...", false, true, null);
+                        });
+                    }
+                }
+            }
+        }
+
+        private void DecrementUpdateCount ()
+        {
+            lock (sync) {
+                if (!Disposed) {
+                    if (--update_count == 0) {
+                        ThreadAssist.ProxyToMain (delegate {
+                            source.HideStatus ();
+                        });
+                    }
+                }
+            }
+        }
+
+        private void RefreshArtworkFor (PaasChannel channel)
+        {
+            if (channel.LastDownloadTime != DateTime.MinValue && !CoverArtSpec.CoverExists (ArtworkIdFor (channel))) {
+                Banshee.Kernel.Scheduler.Schedule (new PaasImageFetchJob (channel), Banshee.Kernel.JobPriority.BelowNormal);
+            }
+        }
+
+        public static string ArtworkIdFor (PaasChannel channel)
+        {
+            return ArtworkIdFor (channel.Name);
+        }
+
+        public static string ArtworkIdFor (string id)
+        {
+            return String.Format ("paas-{0}", Banshee.Base.CoverArtSpec.EscapePart (id));
+        }
+
+        public static readonly SchemaEntry<string> MiroGuideUsername = new SchemaEntry<string> (
+            "plugins.paas.miroguide", "username", String.Empty, "Miro Guide Username", ""
+        );
+
+        public static readonly SchemaEntry<string> MiroGuidePasswordHash = new SchemaEntry<string> (
+            "plugins.paas.miroguide", "password_hash", String.Empty, "Miro Guide Password Hash", ""
+        );
+
+        public static readonly SchemaEntry<string> MiroGuideSessionID = new SchemaEntry<string> (
+            "plugins.paas.miroguide", "session_id", String.Empty, "Miro Guide Session ID", ""
+        );
+
+        public static readonly SchemaEntry<string> MiroGuideServiceUri = new SchemaEntry<string> (
+            "plugins.paas.miroguide", "service_uri", "http://miroguide.com";, "Miro Guide Service URI", ""
+        );
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas/PaasSource.cs b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas/PaasSource.cs
new file mode 100644
index 0000000..387cef3
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Banshee.Paas/Banshee.Paas/PaasSource.cs
@@ -0,0 +1,277 @@
+//
+// PaasSource.cs
+//
+// Authors:
+//   Mike Urbanski <michael c urbanski gmail com>
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009 Michael C. Urbanski
+// Copyright (C) 2008 Novell, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Hyena.Collections;
+
+using Banshee.Base;
+using Banshee.Sources;
+using Banshee.Sources.Gui;
+using Banshee.ServiceStack;
+
+using Banshee.Collection;
+using Banshee.Collection.Gui;
+using Banshee.Collection.Database;
+
+using Migo2.Async;
+
+using Banshee.Paas.Gui;
+using Banshee.Paas.Data;
+
+namespace Banshee.Paas
+{
+    public class PaasSource : Banshee.Library.LibrarySource
+    {
+        private RateLimiter redraw_limiter;
+
+        private PaasActions actions = null;
+        private PaasSourceContents contents;
+        private PaasChannelModel channel_model;
+
+        protected override bool HasArtistAlbum {
+            get { return false; }
+        }
+
+        public override bool AcceptsInputFromSource (Source source)
+        {
+            return false;
+        }
+
+        public override string DefaultBaseDirectory {
+            get {
+                // HACK there isn't an XDG_PODCASTS_DIR; propose it?
+                return XdgBaseDirectorySpec.GetUserDirectory ("XDG_PODCASTS_DIR", "Podcasts");
+            }
+        }
+
+        public override bool CanRename {
+            get { return false; }
+        }
+
+        public override bool CanAddTracks {
+            get { return true; }
+        }
+
+        public override bool CanRemoveTracks {
+            get { return false; }
+        }
+
+        public override bool CanDeleteTracks {
+            get { return false; }
+        }
+
+        public PaasChannelModel ChannelModel {
+            get { return channel_model; }
+        }
+
+        public override string PreferencesPageId {
+            get { return UniqueId; }
+        }
+
+        public override bool ShowBrowser {
+            get { return true; }
+        }
+
+        public override SourceMergeType SupportedMergeTypes {
+            get { return SourceMergeType.None; }
+        }
+
+#region Constructors
+        public PaasSource (PaasService service) : base (Catalog.GetString ("Podcasts"), "PaasLibrary", 200)
+        {
+            actions = new PaasActions (service);
+
+            SupportsPlaylists = false;
+            TrackExternalObjectHandler = GetPaasTrackInfo;
+            TrackArtworkIdHandler = GetTrackArtworkId;
+            MediaTypes = TrackMediaAttributes.Podcast;
+            NotMediaTypes = TrackMediaAttributes.AudioBook;
+            SyncCondition = "(substr(CoreTracks.Uri, 0, 4) != 'http' AND CoreTracks.PlayCount = 0)";
+
+            Properties.SetString ("Icon.Name", "podcast");
+
+            Properties.SetString ("ActiveSourceUIResource", "ActiveSourceUI.xml");
+            Properties.Set<bool> ("ActiveSourceUIResourcePropagate", false);
+            Properties.Set<System.Reflection.Assembly> ("ActiveSourceUIResource.Assembly", typeof(PaasSource).Assembly);
+
+            Properties.SetString ("GtkActionPath", "/PaasSourcePopup");
+
+            contents = new PaasSourceContents ();
+
+            (contents.TrackView as PaasItemView).PopupMenu += OnItemViewPopupMenuHandler;
+            (contents.TrackView as PaasItemView).FuckedPopupMenu += OnItemViewFuckedPopupMenuHandler;
+
+            Properties.Set<ISourceContents> ("Nereid.SourceContents", contents);
+            Properties.Set<bool> ("Nereid.SourceContentsPropagate", false);
+
+            PaasColumnController column_controller = new PaasColumnController ();
+
+            column_controller.SetIndicatorColumnDataHelper (
+                (cell, item) => {
+                    PaasItem pi = item as PaasItem;
+
+                    return (pi != null) ?
+                        service.DownloadManager.CheckActiveDownloadStatus (pi.DbId) :
+                        TaskState.None;
+                }
+            );
+
+            contents.ChannelView.SetChannelDataHelper (
+                (cell, channel) => service.SyndicationClient.GetUpdateStatus (channel as PaasChannel)
+            );
+
+            Properties.Set<PaasColumnController> ("TrackView.ColumnController", column_controller);
+
+            redraw_limiter = new RateLimiter (() => {
+                ThreadAssist.ProxyToMain (() => {
+                    contents.QueueDraw ();
+                });
+            });
+        }
+
+#endregion
+
+        public void AddItem (PaasItem item)
+        {
+            if (item != null) {
+                PaasTrackInfo pti = new PaasTrackInfo (new DatabaseTrackInfo (), item);
+                pti.Track.PrimarySource = this;
+
+                pti.Track.Save (false);
+            }
+        }
+
+        public void AddItems (IEnumerable<PaasItem> items)
+        {
+            if (items != null) {
+                foreach (PaasItem item in items) {
+                    AddItem (item);
+                }
+            }
+        }
+
+        public void RemoveItem (PaasItem item)
+        {
+            if (item != null) {
+                RemoveTrack ((int)item.DbId);
+            }
+        }
+
+        public void RemoveItems (IEnumerable<PaasItem> items)
+        {
+            if (items != null) {
+                RangeCollection rc = new RangeCollection ();
+
+                foreach (PaasItem item in items) {
+                    rc.Add ((int)item.DbId);
+                }
+
+                foreach (RangeCollection.Range range in rc.Ranges) {
+                    RemoveTrackRange (TrackModel as DatabaseTrackListModel, range);
+                }
+            }
+        }
+
+        public void QueueDraw ()
+        {
+            redraw_limiter.Execute ();
+        }
+
+        protected override IEnumerable<IFilterListModel> CreateFiltersFor (DatabaseSource src)
+        {
+            PaasChannelModel channel_model = new PaasChannelModel (
+                src, src.DatabaseTrackModel, ServiceManager.DbConnection,
+                String.Format ("PaasChannels-{0}", src.UniqueId)
+            );
+
+            channel_model.Selection.Changed += (sender, e) => { actions.UpdateChannelActions (); };
+
+            yield return channel_model;
+
+            yield return new PaasUnheardFilterModel (src.DatabaseTrackModel);
+            yield return new DownloadStatusFilterModel (src.DatabaseTrackModel);
+
+            if (src == this) {
+                this.channel_model = channel_model;
+                AfterInitialized ();
+            }
+        }
+
+        public override void Dispose ()
+        {
+            if (actions != null) {
+                actions.Dispose ();
+                actions = null;
+            }
+
+            (contents.TrackView as PaasItemView).PopupMenu -= OnItemViewPopupMenuHandler;
+            (contents.TrackView as PaasItemView).FuckedPopupMenu -= OnItemViewFuckedPopupMenuHandler;
+
+            base.Dispose ();
+        }
+
+        protected override void RemoveTrackRange (DatabaseTrackListModel model, RangeCollection.Range range)
+        {
+            ServiceManager.DbConnection.Execute (
+                String.Format (remove_range_sql,
+                    "TrackID FROM CoreTracks WHERE PrimarySourceID = ? AND ExternalID >= ? AND ExternalID <= ?"
+                ), DateTime.Now, DbId, range.Start, range.End, DbId, range.Start, range.End
+            );
+        }
+
+        private object GetPaasTrackInfo (DatabaseTrackInfo track)
+        {
+            return new PaasTrackInfo (track);
+        }
+
+        protected override DatabaseTrackListModel CreateTrackModelFor (DatabaseSource src)
+        {
+            return new PaasTrackListModel (ServiceManager.DbConnection, DatabaseTrackInfo.Provider, src);
+        }
+
+        [GLib.ConnectBefore]
+        private void OnItemViewPopupMenuHandler (object sender, Gtk.PopupMenuArgs e)
+        {
+            actions.UpdateItemActions ();
+        }
+
+        private void OnItemViewFuckedPopupMenuHandler (object sender, EventArgs e)
+        {
+            actions.UpdateItemActions ();
+        }
+
+        private string GetTrackArtworkId (DatabaseTrackInfo track)
+        {
+            return PaasService.ArtworkIdFor (PaasTrackInfo.From (track).Channel);
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Paas/Makefile.am b/src/Extensions/Banshee.Paas/Makefile.am
new file mode 100644
index 0000000..80b04f5
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Makefile.am
@@ -0,0 +1,123 @@
+ASSEMBLY = Banshee.Paas
+TARGET = library
+LINK = $(REF_EXTENSION_PAAS)
+INSTALL_DIR = $(EXTENSIONS_INSTALL_DIR)
+
+SOURCES =  \
+	Banshee.Paas/Banshee.Paas.Aether/AetherClient.cs \
+	Banshee.Paas/Banshee.Paas.Aether/AetherClientID.cs \
+	Banshee.Paas/Banshee.Paas.Aether/AetherClientState.cs \
+	Banshee.Paas/Banshee.Paas.Aether/AetherClientStateChangedEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/AetherRequest.cs \
+	Banshee.Paas/Banshee.Paas.Aether/AetherRequestCompletedEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/ChannelEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/ChannelUpdateStatus.cs \
+	Banshee.Paas/Banshee.Paas.Aether/ItemEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/GetCategoriesCompletedEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/GetChannelsEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideAccountInfo.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideCategoryInfo.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideChannelInfo.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClient.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClientError.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideClientMethod.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideFilterType.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideMethodCompletedEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideRequestState.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/MiroGuideSortType.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/RequestCompletedEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/SearchContext.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/ServiceMethodFlags.cs \
+	Banshee.Paas/Banshee.Paas.Aether/MiroGuideClient/SubscriptionRequestedEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/RequestState.cs \
+	Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateCompletedEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateManager.cs \
+	Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ChannelUpdateTask.cs \
+	Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/ItmsPodcast.cs \
+	Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/RssParser.cs \
+	Banshee.Paas/Banshee.Paas.Aether/SyndicationClient/SyndicationClient.cs \
+	Banshee.Paas/Banshee.Paas.Data/CacheableItem.cs \
+	Banshee.Paas/Banshee.Paas.Data/CacheModelProvider.cs \
+	Banshee.Paas/Banshee.Paas.Data/DownloadPreference.cs \
+	Banshee.Paas/Banshee.Paas.Data/DownloadStatusFilterModel.cs \
+	Banshee.Paas/Banshee.Paas.Data/ListModel.cs \
+	Banshee.Paas/Banshee.Paas.Data/PaasChannel.cs \
+	Banshee.Paas/Banshee.Paas.Data/PaasChannelModel.cs \
+	Banshee.Paas/Banshee.Paas.Data/PaasItem.cs \
+	Banshee.Paas/Banshee.Paas.Data/PaasTrackInfo.cs \
+	Banshee.Paas/Banshee.Paas.Data/PaasTrackListModel.cs \
+	Banshee.Paas/Banshee.Paas.Data/PaasUnheardFilterModel.cs \
+	Banshee.Paas/Banshee.Paas.Data/SingletonSelection.cs \
+	Banshee.Paas/Banshee.Paas.DownloadManager.Data/QueuedDownloadTask.cs \
+	Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadListView.cs \
+	Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadManagerInterface.cs \
+	Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadSource.cs \
+	Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadSourceContents.cs \
+	Banshee.Paas/Banshee.Paas.DownloadManager.Gui/DownloadUserJob.cs \
+	Banshee.Paas/Banshee.Paas.DownloadManager/DownloadListModel.cs \
+	Banshee.Paas/Banshee.Paas.DownloadManager/PaasDownloadManager.cs \
+	Banshee.Paas/Banshee.Paas.Gui/ColumnCellChannel.cs \
+	Banshee.Paas/Banshee.Paas.Gui/ColumnCellDownloadStatus.cs \
+	Banshee.Paas/Banshee.Paas.Gui/ColumnCellPaasStatusIndicator.cs \
+	Banshee.Paas/Banshee.Paas.Gui/ColumnCellPublished.cs \
+	Banshee.Paas/Banshee.Paas.Gui/ColumnCellUnheard.cs \
+	Banshee.Paas/Banshee.Paas.Gui/Dialogs/ChannelPropertiesDialog.cs \
+	Banshee.Paas/Banshee.Paas.Gui/Dialogs/SubscribeDialog.cs \
+	Banshee.Paas/Banshee.Paas.Gui/DownloadPreferenceComboBox.cs \
+	Banshee.Paas/Banshee.Paas.Gui/DownloadStatusFilterView.cs \
+	Banshee.Paas/Banshee.Paas.Gui/IColumnCellDataHelper.cs \
+	Banshee.Paas/Banshee.Paas.Gui/PaasActions.cs \
+	Banshee.Paas/Banshee.Paas.Gui/PaasChannelView.cs \
+	Banshee.Paas/Banshee.Paas.Gui/PaasColumnController.cs \
+	Banshee.Paas/Banshee.Paas.Gui/PaasItemPage.cs \
+	Banshee.Paas/Banshee.Paas.Gui/PaasItemView.cs \
+	Banshee.Paas/Banshee.Paas.Gui/PaasSourceContents.cs \
+	Banshee.Paas/Banshee.Paas.Gui/PaasUnheardFilterView.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ChannelInfoPreview.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ColumnCellChannel.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideAccountDialog.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideActions.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideCategoryListView.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideChannelListView.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideLoginForm.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/MiroGuideSearchEntry.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/ReflectionInfoWidget.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SortPreferenceActionButton.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SortPreferenceChangedEventArgs.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/BrowserSourceContents.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/ChannelSourceContents.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide.Gui/SourceContents/MiroGuideSourceContents.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideCategoryListModel.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideChannelListModel.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideImageFetchJob.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideInterfaceManager.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/MiroGuideSearchFilter.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/BrowseChannelsSource.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/ChannelSource.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/FeaturedChannelsSource.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/HDChannelsSource.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/MiroGuideSource.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/MiroGuideSourcePosition.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/PopularChannelsSource.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/RecommendedChannelsSource.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/SearchSource.cs \
+	Banshee.Paas/Banshee.Paas.MiroGuide/Sources/TopRatedChannelsSource.cs \
+	Banshee.Paas/Banshee.Paas.Utils/OpmlParser.cs \
+	Banshee.Paas/Banshee.Paas.Utils/StringUtils.cs \
+	Banshee.Paas/Banshee.Paas/PaasImageFetchJob.cs \
+	Banshee.Paas/Banshee.Paas/PaasService.cs \
+	Banshee.Paas/Banshee.Paas/PaasSource.cs
+
+RESOURCES =  \
+	Banshee.Paas.addin.xml \
+	Resources/ActiveSourceUI.xml \
+	Resources/MiroGuideActiveSourceUI.xml \
+	Resources/GlobalUI.xml \
+	Resources/MiroGuideUI.xml
+
+if ENABLE_PAAS
+include $(top_srcdir)/build/build.mk
+else
+EXTRA_DIST = $(SOURCES) $(RESOURCES)
+endif
+
diff --git a/src/Extensions/Banshee.Paas/Resources/ActiveSourceUI.xml b/src/Extensions/Banshee.Paas/Resources/ActiveSourceUI.xml
new file mode 100644
index 0000000..1e5206e
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Resources/ActiveSourceUI.xml
@@ -0,0 +1,39 @@
+<ui>
+  <toolbar name="HeaderToolbar">
+    <placeholder name="SourceActions">
+      <toolitem name="PaasSubscribe" action="PaasSubscribeAction" />
+      <toolitem name="PaasUpdateAll" action="PaasUpdateAllAction" />
+    </placeholder>
+  </toolbar>
+
+  <menubar name="MainMenu" action="MainMenuAction">
+    <menu name="MediaMenu" action="MediaMenuAction">
+      <placeholder name="BelowOpenLocation">
+        <separator />      
+        <menuitem name="PaasSubscribe" action="PaasSubscribeAction" />
+        <separator />              
+        <menuitem name="PaasExportAllOpml" action="PaasExportAllOpmlAction" />                  
+        <menuitem name="PaasImportOpml" action="PaasImportOpmlAction" />       
+      </placeholder>
+    </menu>
+  </menubar>
+
+  <popup name="TrackContextMenu" action="TrackContextMenuAction">
+    <placeholder name="BelowAddToPlaylist">
+      <separator />
+      <menuitem name="PaastItemLink" action="PaasItemLinkAction" />
+      <separator />      
+      <menuitem name="PaasItemPause" action="PaasItemPauseAction" />
+      <menuitem name="PaasItemResume" action="PaasItemResumeAction" />                
+      <menuitem name="PaasItemCancel" action="PaasItemCancelAction" />
+      <menuitem name="PaasItemDownload" action="PaasItemDownloadAction" />
+      <separator />
+      <menuitem name="PaasItemRemove" action="PaasItemRemoveAction" />
+      <menuitem name="PaasItemDelete" action="PaasItemDeleteAction" />      
+      <separator />        
+      <menuitem name="PaasItemMarkNew" action="PaasItemMarkNewAction" />
+      <menuitem name="PaasItemMarkOld" action="PaasItemMarkOldAction" />
+      <separator />
+    </placeholder>
+  </popup>
+</ui>
diff --git a/src/Extensions/Banshee.Paas/Resources/GlobalUI.xml b/src/Extensions/Banshee.Paas/Resources/GlobalUI.xml
new file mode 100644
index 0000000..893c7b3
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Resources/GlobalUI.xml
@@ -0,0 +1,43 @@
+<ui>
+  <popup name="PaasSourcePopup" action="PaasSourcePopupAction">
+    <menuitem name="PaasSubscribe" action="PaasSubscribeAction" />
+    <separator />      
+    <menuitem name="PaasExportAllOpml" action="PaasExportAllOpmlAction" />
+    <menuitem name="PaasImportOpml" action="PaasImportOpmlAction" />
+    <separator />
+<!--    <menuitem name="PaasMiroGuideSubscribeAll" action="PaasMiroGuideSubscribAlleAction" /> 
+    <menuitem name="PaasMiroGuideGetSubscriptions" action="PaasMiroGuideGetSubscriptionsAction" /> -->   
+    <separator />    
+    <menuitem name="PaasUpdateAll" action="PaasUpdateAllAction" />
+    <menuitem name="PaasDownloadAll" action="PaasDownloadAllAction" />
+    <separator />    
+    <menuitem name="SourcePreferences" action="SourcePreferencesAction"/>
+  </popup>
+
+  <popup name="PaasChannelPopup" action="PaasChannelPopupAction">
+    <menuitem name="PaasChannelUpdate" action="PaasChannelUpdateAction" />
+    <separator />        
+    <menuitem name="PaasChannelHomepage" action="PaasChannelHomepageAction" />
+    <separator />
+    <menuitem name="PaasExportOpml" action="PaasExportOpmlAction" />
+<!--    <menuitem name="PaasMiroGuideSubscribe" action="PaasMiroGuideSubscribeAction" /> -->
+    <separator />    
+    <menuitem name="PaasChannelDownloadAll" action="PaasChannelDownloadAllAction"/>        
+    <menuitem name="PaasChannelDelete" action="PaasChannelDeleteAction" />
+    <separator />
+    <menuitem name="PaasChannelProperties" action="PaasChannelPropertiesAction" />
+  </popup>
+
+  <popup name="PaasAllChannelsContextMenu" action="PaasAllChannelsContextMenuAction">
+    <menuitem name="PaasSubscribe" action="PaasSubscribeAction" />
+    <separator />          
+    <menuitem name="PaasExportAllOpml" action="PaasExportAllOpmlAction" />
+    <menuitem name="PaasImportOpml" action="PaasImportOpmlAction" />
+    <separator />
+<!--    <menuitem name="PaasMiroGuideSubscribeAll" action="PaasMiroGuideSubscribAlleAction" /> 
+    <menuitem name="PaasMiroGuideGetSubscriptions" action="PaasMiroGuideGetSubscriptionsAction" /> -->     
+    <separator />    
+    <menuitem name="PaasUpdateAll" action="PaasUpdateAllAction" />
+    <menuitem name="PaasDownloadAll" action="PaasDownloadAllAction" />  
+  </popup>
+</ui>
diff --git a/src/Extensions/Banshee.Paas/Resources/MiroGuideActiveSourceUI.xml b/src/Extensions/Banshee.Paas/Resources/MiroGuideActiveSourceUI.xml
new file mode 100644
index 0000000..74801cd
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Resources/MiroGuideActiveSourceUI.xml
@@ -0,0 +1,7 @@
+<ui>
+  <toolbar name="HeaderToolbar">
+    <placeholder name="SourceActions">
+      <toolitem name="MiroGuideRefreshChannels" action="MiroGuideRefreshChannelsAction" />
+    </placeholder>
+  </toolbar>
+</ui>
\ No newline at end of file
diff --git a/src/Extensions/Banshee.Paas/Resources/MiroGuideUI.xml b/src/Extensions/Banshee.Paas/Resources/MiroGuideUI.xml
new file mode 100644
index 0000000..aaff589
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/Resources/MiroGuideUI.xml
@@ -0,0 +1,23 @@
+<ui>
+  <toolbar name="FooterToolbar">
+    <placeholder name="Extensions">
+        <placeholder name="MiroGuideChannelSortButton" />
+    </placeholder>
+  </toolbar>
+<!--
+  <popup name="MiroGuideSourcePopup" action="MiroGuideSourcePopupAction">
+    <menuitem name="PaasEditMiroGuideProperties" action="PaasEditMiroGuidePropertiesAction" />    
+  </popup>
+-->
+  <popup name="MiroGuideChannelPopup" action="MiroGuideChannelPopupAction">
+    <menuitem name="MiroGuideChannelSubscribe" action="MiroGuideChannelSubscribeAction" />
+  </popup>
+
+  <popup name="MiroGuideSortPreferencePopup" action="MiroGuideSortPreferencePopupAction">
+    <menuitem name="MiroGuideSortByName" action="MiroGuideSortByNameAction" />
+    <separator />        
+    <menuitem name="MiroGuideSortByRating" action="MiroGuideSortByRatingAction" />
+    <menuitem name="MiroGuideSortByPopularity" action="MiroGuideSortByPopularityAction" />
+    <menuitem name="MiroGuideSortByRelevance" action="MiroGuideSortByRelevanceAction" />        
+  </popup>
+</ui>
diff --git a/src/Extensions/Banshee.Paas/ThemeIcons/16x16/categories/podcast.png b/src/Extensions/Banshee.Paas/ThemeIcons/16x16/categories/podcast.png
new file mode 100644
index 0000000..f07b8ed
Binary files /dev/null and b/src/Extensions/Banshee.Paas/ThemeIcons/16x16/categories/podcast.png differ
diff --git a/src/Extensions/Banshee.Paas/ThemeIcons/16x16/status/podcast-new.png b/src/Extensions/Banshee.Paas/ThemeIcons/16x16/status/podcast-new.png
new file mode 100644
index 0000000..8cf4dea
Binary files /dev/null and b/src/Extensions/Banshee.Paas/ThemeIcons/16x16/status/podcast-new.png differ
diff --git a/src/Extensions/Banshee.Paas/ThemeIcons/22x22/categories/podcast.png b/src/Extensions/Banshee.Paas/ThemeIcons/22x22/categories/podcast.png
new file mode 100644
index 0000000..98344a1
Binary files /dev/null and b/src/Extensions/Banshee.Paas/ThemeIcons/22x22/categories/podcast.png differ
diff --git a/src/Extensions/Banshee.Paas/ThemeIcons/48x48/categories/podcast.png b/src/Extensions/Banshee.Paas/ThemeIcons/48x48/categories/podcast.png
new file mode 100644
index 0000000..7c85469
Binary files /dev/null and b/src/Extensions/Banshee.Paas/ThemeIcons/48x48/categories/podcast.png differ
diff --git a/src/Extensions/Banshee.Paas/ThemeIcons/scalable/categories/miro-browse.svg b/src/Extensions/Banshee.Paas/ThemeIcons/scalable/categories/miro-browse.svg
new file mode 100644
index 0000000..744931d
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/ThemeIcons/scalable/categories/miro-browse.svg
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   version="1.1"
+   width="48"
+   height="48"
+   id="svg3033">
+  <defs
+     id="defs3035">
+    <linearGradient
+       x1="22.885227"
+       y1="17.628952"
+       x2="22.885227"
+       y2="30.889549"
+       id="linearGradient2967"
+       xlink:href="#linearGradient3202"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.0395395,0,0,0.9504295,-44.269588,47.458983)" />
+    <linearGradient
+       id="linearGradient3202">
+      <stop
+         id="stop3204"
+         style="stop-color:#ffffff;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3206"
+         style="stop-color:#d3eefc;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       x1="25.579119"
+       y1="-25.736307"
+       x2="25.579119"
+       y2="41.953453"
+       id="linearGradient2922"
+       xlink:href="#linearGradient3211"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.96423685,0,0,0.97292031,-0.65526887,1.99877)" />
+    <linearGradient
+       id="linearGradient3211">
+      <stop
+         id="stop3213"
+         style="stop-color:#ffffff;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3215"
+         style="stop-color:#ffffff;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       cx="39.651478"
+       cy="18.618757"
+       r="20.714195"
+       fx="39.651478"
+       fy="18.618757"
+       id="radialGradient2926"
+       xlink:href="#linearGradient3242-2"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.04588502,2.9168208,-4.3002473,3.3179656e-7,75.324517,-119.13082)" />
+    <linearGradient
+       id="linearGradient3242-2">
+      <stop
+         id="stop3244"
+         style="stop-color:#f8b17e;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3246-9"
+         style="stop-color:#e35d4f;stop-opacity:1"
+         offset="0.31209752" />
+      <stop
+         id="stop3248-3"
+         style="stop-color:#c6262e;stop-opacity:1"
+         offset="0.57054454" />
+      <stop
+         id="stop3250-9"
+         style="stop-color:#690b54;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       x1="24.008854"
+       y1="38.246284"
+       x2="24.008854"
+       y2="0.99999988"
+       id="linearGradient2928"
+       xlink:href="#linearGradient2490"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       id="linearGradient2490">
+      <stop
+         id="stop2492"
+         style="stop-color:#791235;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop2494"
+         style="stop-color:#dd3b27;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       x1="21.13592"
+       y1="40.884956"
+       x2="21.13592"
+       y2="35.298134"
+       id="linearGradient2932"
+       xlink:href="#linearGradient2346"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.89432531,0,0,0.71689518,3.0387027,11.694937)" />
+    <linearGradient
+       id="linearGradient2346">
+      <stop
+         id="stop2348"
+         style="stop-color:#eeeeee;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop2350"
+         style="stop-color:#d9d9da;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       x1="302.85715"
+       y1="366.64789"
+       x2="302.85715"
+       y2="609.50507"
+       id="linearGradient3012"
+       xlink:href="#linearGradient5048"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.774389,0,0,1.969706,-1892.179,-872.8854)" />
+    <linearGradient
+       id="linearGradient5048">
+      <stop
+         id="stop5050"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="0" />
+      <stop
+         id="stop5056"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0.5" />
+      <stop
+         id="stop5052"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       cx="605.71429"
+       cy="486.64789"
+       r="117.14286"
+       fx="605.71429"
+       fy="486.64789"
+       id="radialGradient3014"
+       xlink:href="#linearGradient5060"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.774389,0,0,1.969706,-1891.633,-872.8854)" />
+    <linearGradient
+       id="linearGradient5060">
+      <stop
+         id="stop5062"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop5064"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       cx="605.71429"
+       cy="486.64789"
+       r="117.14286"
+       fx="605.71429"
+       fy="486.64789"
+       id="radialGradient3031"
+       xlink:href="#linearGradient5060"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-2.774389,0,0,1.969706,112.7623,-872.8854)" />
+  </defs>
+  <g
+     id="layer1">
+    <g
+       transform="matrix(0.02146165,0,0,0.01463898,43.088761,43.203389)"
+       id="g8875">
+      <rect
+         width="1339.6335"
+         height="478.35718"
+         x="-1559.2523"
+         y="-150.69685"
+         id="rect8877"
+         style="opacity:0.40206185;fill:url(#linearGradient3012);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible" />
+      <path
+         d="m -219.61876,-150.68038 c 0,0 0,478.33079 0,478.33079 142.874166,0.90045 345.40022,-107.16966 345.40014,-239.196175 0,-132.026537 -159.436816,-239.134595 -345.40014,-239.134615 z"
+         id="path8879"
+         style="opacity:0.40206185;fill:url(#radialGradient3014);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible" />
+      <path
+         d="m -1559.2523,-150.68038 c 0,0 0,478.33079 0,478.33079 -142.8742,0.90045 -345.4002,-107.16966 -345.4002,-239.196175 0,-132.026537 159.4368,-239.134595 345.4002,-239.134615 z"
+         id="path8881"
+         style="opacity:0.40206185;fill:url(#radialGradient3031);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible" />
+    </g>
+    <path
+       d="m 43.271179,44.500125 -37.4219627,0 c -0.347147,0 -0.6266189,-0.317795 -0.6266189,-0.712548 L 4.4998733,38.285815 c 0,-0.394753 0.2794721,-0.712547 0.6266191,-0.712547 l 38.1037726,0"
+       id="rect8840"
+       style="fill:url(#linearGradient2932);fill-opacity:1;fill-rule:nonzero;stroke:#7a1235;stroke-width:0.99974567;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0.06;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+    <path
+       d="m 42.811302,43.623944 c -0.444161,-0.581595 -0.26312,-3.849289 0.601434,-5.306799"
+       id="path9017"
+       style="fill:none;stroke:#666666;stroke-width:0.99974567;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+    <path
+       d="m 7.0900094,1.4998728 c -0.3552903,0 -0.6318528,0.4060416 -0.6318524,0.9204463 L 4.5175814,36.825966 c 0,0.514406 0.3040344,0.920448 0.6593242,0.920447 l 37.6638974,0 c 0.355288,1e-6 0.659324,-0.406041 0.659324,-0.920447 l -2.1702,-34.4056469 c 1e-6,-0.5144047 -0.276562,-0.9204463 -0.631853,-0.9204463 l -33.6080646,0 z"
+       id="rect8064"
+       style="fill:url(#radialGradient2926);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2928);stroke-width:0.99974567;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0.06;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+    <path
+       d="M 6.2186139,43.454184 5.5983146,38.823012"
+       id="path3267"
+       style="opacity:0.2;fill:none;stroke:#000000;stroke-width:0.99974567px;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       d="m 8.0546969,2.4733527 c -0.3552905,0 -0.6318528,0.4060416 -0.6318524,0.9204463 L 5.4822687,35.85311 c 0,0.514405 0.3040346,0.920448 0.6593244,0.920447 l 35.7349339,0 c 0.355288,1e-6 0.659323,-0.406042 0.659323,-0.920447 L 40.36565,3.393799 c 10e-7,-0.5144047 -0.276561,-0.9204463 -0.631852,-0.9204463 l -31.6791011,0 z"
+       id="path2868"
+       style="opacity:0.4;fill:none;stroke:url(#linearGradient2922);stroke-width:0.99974567;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0.06;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+    <g
+       transform="translate(43.198023,-50.872728)"
+       id="g2963">
+      <path
+         d="m -30.690913,76.872727 c 1.225378,0 3.774622,0 5,0 0.02003,-2.69044 -0.08349,-5.382579 0,-8.07176 0,-1.434446 0.209448,-2.71704 1.896799,-2.71704 1.935845,0 2.089684,0.969817 2.089684,2.817071 0,1.688236 0,6.283493 0,7.971729 1.225377,0 3.78814,10e-7 5.013517,10e-7 0.01437,-2.645706 -0.05881,-5.292395 0,-7.937385 0.0089,-1.27218 -0.113326,-2.851416 1.992422,-2.851416 1.894438,0 2.006642,1.246514 2.006642,2.872766 0,1.66967 -1e-6,6.246363 0,7.916035 1.225376,0 3.775558,0 5.000936,0 -0.01422,-2.951874 0.02968,-5.904782 -0.02429,-8.855952 -0.07686,-1.784335 -0.762887,-3.755827 -2.587981,-4.659627 -1.044123,-0.342811 -1.342666,-0.472622 -2.644984,-0.472622 -2.682008,0 -13.090101,-0.0118 -17.742745,-0.0118 0,4.666666 0,9.333334 0,13.999999 z"
+         id="path2438"
+         style="font-size:26.70637703px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#a1ceef;fill-opacity:1;stroke:none;font-family:URW Gothic L;-inkscape-font-specification:URW Gothic L Book" />
+      <path
+         d="m -30.690913,77.872727 c 1.225378,0 3.774622,0 5,0 0.02003,-2.69044 -0.08349,-5.382579 0,-8.07176 0,-1.434446 0.209448,-2.71704 1.896799,-2.71704 1.935845,0 2.089684,0.969817 2.089684,2.817071 0,1.688236 0,6.283493 0,7.971729 1.225377,0 3.78814,10e-7 5.013517,10e-7 0.01437,-2.645706 -0.05881,-5.292395 0,-7.937385 0.0089,-1.27218 -0.113326,-2.851416 1.992422,-2.851416 1.894438,0 2.006642,1.246514 2.006642,2.872766 0,1.66967 -1e-6,6.246363 0,7.916035 1.225376,0 3.775558,0 5.000936,0 -0.01422,-2.951874 0.02968,-5.904782 -0.02429,-8.855952 -0.07686,-1.784335 -0.762887,-3.755827 -2.587981,-4.659627 -1.044123,-0.342811 -1.342666,-0.472622 -2.644984,-0.472622 -2.682008,0 -13.090101,-0.0118 -17.742745,-0.0118 0,4.666666 0,9.333334 0,13.999999 z"
+         id="text3190"
+         style="font-size:26.70637703px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:url(#linearGradient2967);fill-opacity:1;stroke:none;font-family:URW Gothic L;-inkscape-font-specification:URW Gothic L Book" />
+    </g>
+  </g>
+</svg>
diff --git a/src/Extensions/Banshee.Paas/ThemeIcons/scalable/categories/miro.svg b/src/Extensions/Banshee.Paas/ThemeIcons/scalable/categories/miro.svg
new file mode 100644
index 0000000..be1c328
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/ThemeIcons/scalable/categories/miro.svg
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   version="1.0"
+   width="48"
+   height="48"
+   id="svg3266"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   sodipodi:docname="miro.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <metadata
+     id="metadata33">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     inkscape:window-height="794"
+     inkscape:window-width="1440"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     guidetolerance="10.0"
+     gridtolerance="10.0"
+     objecttolerance="10.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base"
+     showgrid="false"
+     inkscape:zoom="6.8648283"
+     inkscape:cx="-9.6420969"
+     inkscape:cy="24"
+     inkscape:window-x="0"
+     inkscape:window-y="24"
+     inkscape:current-layer="svg3266" />
+  <defs
+     id="defs3268">
+    <linearGradient
+       id="linearGradient8838">
+      <stop
+         id="stop8840"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop8842"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient2490">
+      <stop
+         id="stop2492"
+         style="stop-color:#791235;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop2494"
+         style="stop-color:#dd3b27;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3242">
+      <stop
+         id="stop3244"
+         style="stop-color:#f8b17e;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3246"
+         style="stop-color:#e35d4f;stop-opacity:1"
+         offset="0.26238" />
+      <stop
+         id="stop3248"
+         style="stop-color:#c6262e;stop-opacity:1"
+         offset="0.66093999" />
+      <stop
+         id="stop3250"
+         style="stop-color:#690b54;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3202">
+      <stop
+         id="stop3204"
+         style="stop-color:#ffffff;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3206"
+         style="stop-color:#d3eefc;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3230">
+      <stop
+         id="stop3232"
+         style="stop-color:#ffffff;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3234"
+         style="stop-color:#ffffff;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3230"
+       id="linearGradient2408"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(0,0.9999998)"
+       x1="26.153599"
+       y1="4.9999995"
+       x2="26.153599"
+       y2="44.233311" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3202"
+       id="linearGradient2411"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.0395395,0,0,0.9504295,2.4213254,1.5862551)"
+       x1="22.885227"
+       y1="17.628952"
+       x2="22.885227"
+       y2="30.889549" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3242"
+       id="radialGradient2415"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0,1.6958206,-1.7757718,0,30.273358,-47.575099)"
+       cx="32.806725"
+       cy="3.5327499"
+       fx="32.806725"
+       fy="3.5327499"
+       r="23" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2490"
+       id="linearGradient2417"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(51.417219,1.3502817)"
+       x1="-23.916132"
+       y1="43.707703"
+       x2="-23.916132"
+       y2="4.6497173" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient8838"
+       id="radialGradient2420"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.1647059,0,0,0.8470576,-111.5647,35.082353)"
+       cx="62.625"
+       cy="4.625"
+       fx="62.625"
+       fy="4.625"
+       r="10.625" />
+  </defs>
+  <path
+     style="opacity:0.3;fill:url(#radialGradient2420);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999987999999995;stroke-linecap:butt;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+     id="path8836"
+     d="M 47.000007,39 C 47.000007,43.970562 36.702555,47.999999 24.000007,47.999999 C 11.297459,47.999999 1.0000068,43.970562 1.0000068,39 C 1.0000068,34.029437 11.297459,30 24.000007,30 C 36.702555,30 47.000007,34.029437 47.000007,39 L 47.000007,39 z" />
+  <path
+     style="fill:url(#radialGradient2415);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2417);stroke-width:0.99999988px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="path2421"
+     d="M 1.5000006,31.761226 C 1.5000006,31.761226 10.115608,15.647729 22.458894,9.2294409 C 29.653279,5.4884898 41.244893,4.5285552 46.43013,13.320438 C 46.43013,13.320438 48.176874,36.419951 34.202924,42.922755 C 34.202924,42.922755 19.743769,50.687298 1.5000006,31.761226 z" />
+  <path
+     style="font-size:26.70637703px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#a1ceef;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:URW Gothic L;-inkscape-font-specification:URW Gothic L Book"
+     id="path2438"
+     d="M 16,30.999999 C 17.225378,30.999999 19.774622,30.999999 21,30.999999 C 21.020028,28.309559 20.916511,25.61742 21,22.928239 C 21,21.493793 21.209448,20.211199 22.896799,20.211199 C 24.832644,20.211199 24.986483,21.181016 24.986483,23.02827 C 24.986483,24.716506 24.986483,29.311763 24.986483,30.999999 C 26.21186,30.999999 28.774623,31 30,31 C 30.014374,28.354294 29.941186,25.707605 30,23.062615 C 30.008937,21.790435 29.886674,20.211199 31.992422,20.211199 C 33.88686,20.211199 33.999064,21.457713 33.999064,23.083965 C 33.999064,24.753635 33.999063,29.330328 33.999064,31 C 35.22444,31 37.774622,31 39,31 C 38.985776,28.048126 39.029681,25.095218 38.975706,22.144048 C 38.898843,20.359713 38.212819,18.388221 36.387725,17.484421 C 35.343606,17.14161 35.045063,17.011799 33.742745,17.011799 C 31.060737,17.011799 20.652644,17 16,17 C 16,21.666666 16,26.333334 16,30.999999 z" />
+  <path
+     style="font-size:26.70637703px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:url(#linearGradient2411);fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:URW Gothic L;-inkscape-font-specification:URW Gothic L Book"
+     id="text3190"
+     d="M 16,31.999999 C 17.225378,31.999999 19.774622,31.999999 21,31.999999 C 21.020028,29.309559 20.916511,26.61742 21,23.928239 C 21,22.493793 21.209448,21.211199 22.896799,21.211199 C 24.832644,21.211199 24.986483,22.181016 24.986483,24.02827 C 24.986483,25.716506 24.986483,30.311763 24.986483,31.999999 C 26.21186,31.999999 28.774623,32 30,32 C 30.014374,29.354294 29.941186,26.707605 30,24.062615 C 30.008937,22.790435 29.886674,21.211199 31.992422,21.211199 C 33.88686,21.211199 33.999064,22.457713 33.999064,24.083965 C 33.999064,25.753635 33.999063,30.330328 33.999064,32 C 35.22444,32 37.774622,32 39,32 C 38.985776,29.048126 39.029681,26.095218 38.975706,23.144048 C 38.898843,21.359713 38.212819,19.388221 36.387725,18.484421 C 35.343606,18.14161 35.045063,18.011799 33.742745,18.011799 C 31.060737,18.011799 20.652644,18 16,18 C 16,22.666666 16,27.333334 16,31.999999 z" />
+  <path
+     style="opacity:0.4;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2408);stroke-width:0.99999988px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     id="path3238"
+     d="M 31.90625,7.4999998 C 28.681328,7.7425666 25.48596,8.7210967 22.90625,10.0625 C 11.707044,15.885887 3.6907335,29.825628 2.71875,31.5625 C 11.484066,40.437302 19.207332,43.094404 24.75,43.5 C 30.407274,43.913983 33.75,42.09375 33.75,42.09375 C 33.760173,42.083092 33.770592,42.072673 33.78125,42.0625 C 40.364812,38.998827 43.337844,31.95008 44.625,25.5 C 45.868154,19.27042 45.525212,13.993747 45.5,13.625 C 42.448467,8.6273583 37.213852,7.1007814 31.90625,7.4999998 z" />
+</svg>
diff --git a/src/Extensions/Banshee.Paas/ThemeIcons/scalable/categories/miroguide-default-channel.svg b/src/Extensions/Banshee.Paas/ThemeIcons/scalable/categories/miroguide-default-channel.svg
new file mode 100644
index 0000000..a2b31c4
--- /dev/null
+++ b/src/Extensions/Banshee.Paas/ThemeIcons/scalable/categories/miroguide-default-channel.svg
@@ -0,0 +1,318 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   version="1.1"
+   width="512"
+   height="512"
+   id="svg2"
+   inkscape:version="0.47pre4 r22446"
+   sodipodi:docname="podcast.svg">
+  <metadata
+     id="metadata78">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1280"
+     inkscape:window-height="689"
+     id="namedview76"
+     showgrid="false"
+     inkscape:zoom="0.859375"
+     inkscape:cx="256"
+     inkscape:cy="256"
+     inkscape:window-x="0"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg2" />
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient4386">
+      <stop
+         id="stop4388"
+         style="stop-color:#ffffff;stop-opacity:0"
+         offset="0" />
+      <stop
+         id="stop4390"
+         style="stop-color:#ffffff;stop-opacity:0"
+         offset="0.60000008" />
+      <stop
+         id="stop4392"
+         style="stop-color:#ffffff;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4332">
+      <stop
+         id="stop4334"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="0" />
+      <stop
+         id="stop4336"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="0.60000008" />
+      <stop
+         id="stop4338"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3308-4-6-931-761">
+      <stop
+         id="stop2919"
+         style="stop-color:#ffffff;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop2921"
+         style="stop-color:#ffffff;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient2490-654-721">
+      <stop
+         id="stop2913"
+         style="stop-color:#545454;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop2915"
+         style="stop-color:#a4a4a4;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3242-52-665">
+      <stop
+         id="stop2903"
+         style="stop-color:#d3d3d3;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop2905"
+         style="stop-color:#b1b1b1;stop-opacity:1"
+         offset="0.26238" />
+      <stop
+         id="stop2907"
+         style="stop-color:#8e8e8e;stop-opacity:1"
+         offset="0.66093999" />
+      <stop
+         id="stop2909"
+         style="stop-color:#525252;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       cx="256"
+       cy="541.36218"
+       r="255"
+       fx="256"
+       fy="541.36218"
+       id="radialGradient4189"
+       xlink:href="#linearGradient3308-4-6-931-761"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0,0.11202816,-1.6231217,0,1134.6968,512.68298)" />
+    <radialGradient
+       cx="256"
+       cy="540.36218"
+       r="256"
+       fx="256"
+       fy="540.36218"
+       id="radialGradient4192"
+       xlink:href="#linearGradient3242-52-665"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0,2.2078871,-2.3371329,0,1518.8982,-24.856905)" />
+    <linearGradient
+       x1="96.581818"
+       y1="1052.8691"
+       x2="96.581818"
+       y2="540.36218"
+       id="linearGradient4194"
+       xlink:href="#linearGradient2490-654-721"
+       gradientUnits="userSpaceOnUse" />
+    <filter
+       color-interpolation-filters="sRGB"
+       id="filter4196">
+      <feGaussianBlur
+         id="feGaussianBlur4198"
+         stdDeviation="2.53" />
+    </filter>
+    <radialGradient
+       cx="-206.96829"
+       cy="173.49565"
+       r="33.625023"
+       fx="-206.96829"
+       fy="173.49565"
+       id="radialGradient4330"
+       xlink:href="#linearGradient4332"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(4.8817448,0,0,5.549978,1266.3664,-4.3531002)" />
+    <radialGradient
+       cx="-206.96829"
+       cy="173.49565"
+       r="33.625023"
+       fx="-206.96829"
+       fy="173.49565"
+       id="radialGradient4330-9"
+       xlink:href="#linearGradient4386"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(4.8817431,-2.0310808e-8,2.3379004e-8,5.6191901,1266.3661,-16.36111)" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3242-52-665"
+       id="radialGradient2907"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0,2.2078871,-2.3371329,0,1518.8982,-24.856905)"
+       cx="256"
+       cy="540.36218"
+       fx="256"
+       fy="540.36218"
+       r="256" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2490-654-721"
+       id="linearGradient2909"
+       gradientUnits="userSpaceOnUse"
+       x1="96.581818"
+       y1="1052.8691"
+       x2="96.581818"
+       y2="540.36218" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3308-4-6-931-761"
+       id="radialGradient2911"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0,0.11202816,-1.6231217,0,1134.6968,512.68298)"
+       cx="256"
+       cy="541.36218"
+       fx="256"
+       fy="541.36218"
+       r="255" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient4386"
+       id="radialGradient2913"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(4.8817431,-2.0310808e-8,2.3379004e-8,5.6191901,1266.3661,-16.36111)"
+       cx="-206.96829"
+       cy="173.49565"
+       fx="-206.96829"
+       fy="173.49565"
+       r="33.625023" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient4332"
+       id="radialGradient2915"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(4.8817448,0,0,5.549978,1266.3664,-4.3531002)"
+       cx="-206.96829"
+       cy="173.49565"
+       fx="-206.96829"
+       fy="173.49565"
+       r="33.625023" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3308-4-6-931-761"
+       id="radialGradient2926"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0,0.11202816,-1.6231217,0,1134.6968,-27.6792)"
+       cx="256"
+       cy="541.36218"
+       fx="256"
+       fy="541.36218"
+       r="255" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3242-52-665"
+       id="radialGradient2929"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0,2.2078871,-2.3371329,0,1518.8982,-565.21908)"
+       cx="256"
+       cy="540.36218"
+       fx="256"
+       fy="540.36218"
+       r="256" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2490-654-721"
+       id="linearGradient2931"
+       gradientUnits="userSpaceOnUse"
+       x1="96.581818"
+       y1="1052.8691"
+       x2="96.581818"
+       y2="540.36218"
+       gradientTransform="translate(0,-540.36218)" />
+  </defs>
+  <rect
+     width="511"
+     height="511"
+     rx="8"
+     ry="8"
+     x="0.5"
+     y="0.50000262"
+     id="rect2816"
+     style="color:#000000;fill:url(#radialGradient2929);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2931);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0.06;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+  <rect
+     width="506"
+     height="506"
+     rx="7.9999995"
+     ry="7.9999995"
+     x="3"
+     y="3.0000026"
+     id="rect2816-3"
+     style="opacity:0.59999999999999998;color:#000000;fill:none;stroke:url(#radialGradient2926);stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0.06000000000000000;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+  <g
+     transform="translate(0,-538.36218)"
+     id="g4323-4"
+     style="opacity:0.4">
+    <path
+       d="m -216.43637,207.12727 a 32.58182,32.58182 0 1 1 -65.16364,0 32.58182,32.58182 0 1 1 65.16364,0 z"
+       transform="matrix(1.0128348,0,0,1.0128348,508.21433,557.48554)"
+       id="path4200-6"
+       style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+    <path
+       d="m 413.14879,796.36216 c 0,85.70454 -70.35792,155.18183 -157.14879,155.18183 -86.79087,0 -157.14879,-69.47729 -157.14879,-155.18183 0,-85.70456 70.35792,-155.18179 157.14879,-155.18179 86.79087,0 157.14879,69.47723 157.14879,155.18179 z m -54.3306,-17.45456 c 0,57.4276 -46.0333,103.98183 -102.81819,103.98183 -56.78494,0 -102.81819,-46.55423 -102.81819,-103.98183 0,-57.42755 46.03325,-103.98179 102.81819,-103.98179 56.78489,0 102.81819,46.55424 102.81819,103.98179 z M 316.9273,767.27123 c 0,33.6492 -27.27807,60.92729 -60.9273,60.92729 -33.64918,0 -60.92725,-27.27809 -60.92725,-60.92729 0,-33.64918 27.27807,-60.92723 60.92725,-60.92723 33.64923,0 60.9273,27.27805 60.9273,60.92723 z"
+       id="path4200-9-4-1-3"
+       style="color:#000000;fill:none;stroke:url(#radialGradient2913);stroke-width:14;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+    <path
+       d="m 255.43754,802.59343 c -0.29561,0 -0.581,0.0276 -0.875,0.0312 -18.6857,0.23681 -34.06588,7.93265 -37,18 l -0.53125,0 c -11.16683,29.24646 13.39985,111.58698 17.75,125.6875 0.004,0.0131 -0.004,0.0184 0,0.0312 0.017,3.20857 2.63105,5.78125 5.84375,5.78125 l 1.09375,0 28.5625,0 1.09375,0 c 3.2127,0 5.82675,-2.57268 5.84375,-5.78125 0.004,-0.0129 -0.004,-0.0181 0,-0.0312 4.35015,-14.10052 28.91683,-96.44104 17.75,-125.6875 l -0.53125,0 c -2.93577,-10.07299 -18.33134,-17.77146 -37.03125,-18 -0.28357,-0.003 -0.55869,-0.0312 -0.84375,-0.0312 -0.18635,0 -0.37678,-10e-4 -0.5625,0 -0.18866,-0.002 -0.37319,0 -0.5625,0 z"
+       id="path4280-5"
+       style="color:#000000;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+  </g>
+  <g
+     id="g4323"
+     style="opacity:0.4"
+     transform="translate(0,-540.36218)">
+    <path
+       d="m -216.43637,207.12727 a 32.58182,32.58182 0 1 1 -65.16364,0 32.58182,32.58182 0 1 1 65.16364,0 z"
+       transform="matrix(1.0128348,0,0,1.0128348,508.21433,557.48554)"
+       id="path4200"
+       style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+    <path
+       d="m 413.14879,796.36216 c 0,85.70454 -70.35792,155.18183 -157.14879,155.18183 -86.79087,0 -157.14879,-69.47729 -157.14879,-155.18183 0,-85.70456 70.35792,-155.18179 157.14879,-155.18179 86.79087,0 157.14879,69.47723 157.14879,155.18179 z m -54.3306,-17.45456 c 0,57.4276 -46.0333,103.98183 -102.81819,103.98183 -56.78494,0 -102.81819,-46.55423 -102.81819,-103.98183 0,-57.42755 46.03325,-103.98179 102.81819,-103.98179 56.78489,0 102.81819,46.55424 102.81819,103.98179 z M 316.9273,767.27123 c 0,33.6492 -27.27807,60.92729 -60.9273,60.92729 -33.64918,0 -60.92725,-27.27809 -60.92725,-60.92729 0,-33.64918 27.27807,-60.92723 60.92725,-60.92723 33.64923,0 60.9273,27.27805 60.9273,60.92723 z"
+       id="path4200-9-4-1"
+       style="color:#000000;fill:none;stroke:url(#radialGradient2915);stroke-width:14;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+    <path
+       d="m 255.43754,802.59343 c -0.29561,0 -0.581,0.0276 -0.875,0.0312 -18.6857,0.23681 -34.06588,7.93265 -37,18 l -0.53125,0 c -11.16683,29.24646 13.39985,111.58698 17.75,125.6875 0.004,0.0131 -0.004,0.0184 0,0.0312 0.017,3.20857 2.63105,5.78125 5.84375,5.78125 l 1.09375,0 28.5625,0 1.09375,0 c 3.2127,0 5.82675,-2.57268 5.84375,-5.78125 0.004,-0.0129 -0.004,-0.0181 0,-0.0312 4.35015,-14.10052 28.91683,-96.44104 17.75,-125.6875 l -0.53125,0 c -2.93577,-10.07299 -18.33134,-17.77146 -37.03125,-18 -0.28357,-0.003 -0.55869,-0.0312 -0.84375,-0.0312 -0.18635,0 -0.37678,-10e-4 -0.5625,0 -0.18866,-0.002 -0.37319,0 -0.5625,0 z"
+       id="path4280"
+       style="color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+  </g>
+</svg>
diff --git a/src/Libraries/Migo2/Makefile.am b/src/Libraries/Migo2/Makefile.am
new file mode 100644
index 0000000..a5f9db1
--- /dev/null
+++ b/src/Libraries/Migo2/Makefile.am
@@ -0,0 +1,55 @@
+ASSEMBLY = Migo2
+TARGET = library
+LINK = $(REF_MIGO2)
+SOURCES =  \
+	Migo2.Async/AsyncStateManager.cs \
+	Migo2.Async/CommandQueue/CommandDelegate.cs \
+	Migo2.Async/CommandQueue/CommandQueue.cs \
+	Migo2.Async/CommandQueue/CommandWrapper.cs \
+	Migo2.Async/CommandQueue/EventWrapper.cs \
+	Migo2.Async/CommandQueue/ICommand.cs \
+	Migo2.Async/Task/CancellationType.cs \
+	Migo2.Async/Task/IWaitableTask.cs \
+	Migo2.Async/Task/Task.cs \
+	Migo2.Async/Task/TaskCompletedEventArgs.cs \
+	Migo2.Async/Task/TaskEventArgs.cs \
+	Migo2.Async/Task/TaskState.cs \
+	Migo2.Async/Task/TaskStateChangedEventArgs.cs \
+	Migo2.Async/TaskGroup/EventArgs/GroupStatusChangedEventArgs.cs \
+	Migo2.Async/TaskGroup/EventArgs/ManipulatedEventArgs.cs \
+	Migo2.Async/TaskGroup/EventArgs/ReorderedEventArgs.cs \
+	Migo2.Async/TaskGroup/EventArgs/TaskAddedEventArgs.cs \
+	Migo2.Async/TaskGroup/EventArgs/TaskProgressChangedEventArgs.cs \
+	Migo2.Async/TaskGroup/EventArgs/TaskRemovedEventArgs.cs \
+	Migo2.Async/TaskGroup/GroupProgressManager.cs \
+	Migo2.Async/TaskGroup/GroupStatusManager.cs \
+	Migo2.Async/TaskGroup/TaskGroup.cs \
+	Migo2.Async/TaskGroup/TaskGroup_Collection.cs \
+	Migo2.Collections/OrderComparer.cs \
+	Migo2.Collections/Pair.cs \
+	Migo2.DownloadService/DownloadStatusManager.cs \
+	Migo2.DownloadService/DownloadTaskStatusUpdatedEventArgs.cs \
+	Migo2.DownloadService/HttpDownloadGroup.cs \
+	Migo2.DownloadService/HttpDownloadGroupStatusChangedEventArgs.cs \
+	Migo2.DownloadService/HttpDownloadManager.cs \
+	Migo2.DownloadService/HttpFileDownloadErrors.cs \
+	Migo2.DownloadService/HttpFileDownloadTask.cs \
+	Migo2.Net/AsyncWebClient/AsyncWebClient.cs \
+	Migo2.Net/AsyncWebClient/DownloadDataCompletedEventArgs.cs \
+	Migo2.Net/AsyncWebClient/DownloadProgressChangedEventArgs.cs \
+	Migo2.Net/AsyncWebClient/DownloadStatus.cs \
+	Migo2.Net/AsyncWebClient/DownloadStatusUpdatedEventArgs.cs \
+	Migo2.Net/AsyncWebClient/DownloadStringCompletedEventArgs.cs \
+	Migo2.Net/AsyncWebClient/RemoteFileModifiedException.cs \
+	Migo2.Net/AsyncWebClient/TransferStatusManager.cs \
+	Migo2.Net/AsyncWebClient/TransferStatusManager_Rate.cs \
+	Migo2.Utils/Rfc822DateTime.cs \
+	Migo2.Utils/UnitUtils.cs \
+	Migo2.Utils/XmlUtils.cs
+
+if ENABLE_PAAS
+include $(top_srcdir)/build/build.mk
+else
+EXTRA_DIST = $(SOURCES) $(RESOURCES)
+endif
+
diff --git a/src/Libraries/Migo2/Migo2.Async/AsyncStateManager.cs b/src/Libraries/Migo2/Migo2.Async/AsyncStateManager.cs
new file mode 100644
index 0000000..d2dbacc
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/AsyncStateManager.cs
@@ -0,0 +1,127 @@
+//
+// AsyncStateManager.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public class AsyncStateManager
+    {
+        private bool busy;
+        private bool completed;
+        private bool cancelled;
+        private bool timedout;
+
+        private readonly object sync = new object ();
+
+        public bool Busy {
+            get { lock (sync) { return busy; } }
+        }
+
+        public bool Cancelled {
+            get { lock (sync) { return cancelled; } }
+        }
+
+        public bool Completed {
+            get { lock (sync) { return completed; } }
+        }
+
+        public bool Timedout {
+            get { lock (sync) { return timedout; } }
+        }
+
+        public AsyncStateManager ()
+        {
+        }
+
+        public void Reset ()
+        {
+            lock (sync) {
+                busy      = false;
+                completed = false;
+                cancelled = false;
+                timedout  = false;
+            }
+        }
+
+        public void ResetBusy      () { lock (sync) { busy      = false; } }
+        public void ResetCancelled () { lock (sync) { cancelled = false; } }
+        public void ResetCompleted () { lock (sync) { completed = false; } }
+        public void ResetTimedOut  () { lock (sync) { timedout  = false; } }
+
+        public bool SetBusy ()
+        {
+            lock (sync) {
+                if (busy) {
+                    throw new InvalidOperationException (
+                        "Concurrent operations are not supported.  Sorry."
+                    );
+                } else {
+                    busy = true;
+                    return true;
+                }
+            }
+        }
+
+        public bool SetCancelled ()
+        {
+            lock (sync) {
+                if (!cancelled && !completed && !timedout) {
+                    cancelled = true;
+                    return true;
+                }
+
+                return false;
+            }
+        }
+
+        public bool SetCompleted ()
+        {
+            lock (sync) {
+                if (busy && !cancelled && !completed && !timedout) {
+                    completed = true;
+
+                    return true;
+                }
+
+                return false;
+            }
+        }
+
+        public bool SetTimedout ()
+        {
+            lock (sync) {
+                if (busy && !cancelled && !completed && !timedout) {
+                    timedout = true;
+
+                    return true;
+                }
+
+                return false;
+            }
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/CommandQueue/CommandDelegate.cs b/src/Libraries/Migo2/Migo2.Async/CommandQueue/CommandDelegate.cs
new file mode 100644
index 0000000..82798c1
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/CommandQueue/CommandDelegate.cs
@@ -0,0 +1,32 @@
+//
+// CommandDelegate.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public delegate void CommandDelegate ();
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/CommandQueue/CommandQueue.cs b/src/Libraries/Migo2/Migo2.Async/CommandQueue/CommandQueue.cs
new file mode 100644
index 0000000..2ce0143
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/CommandQueue/CommandQueue.cs
@@ -0,0 +1,268 @@
+//
+// CommandQueue.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Threading;
+using System.Collections.Generic;
+
+namespace Migo2.Async
+{
+    delegate void ExecuteCommand (ICommand command);
+
+    public class CommandQueue : IDisposable
+    {
+        private bool disposed;
+        private bool executing;
+
+        private Thread queueThread;
+        private Queue<ICommand> eventQueue;
+        private RegisteredWaitHandle registeredHandle;
+        private AutoResetEvent are = new AutoResetEvent (false);
+        private ManualResetEvent executingHandle = new ManualResetEvent (true);
+
+        private readonly ExecuteCommand execCommand;
+
+        private readonly object userSync;
+        private readonly object sync = new object ();
+
+        public EventHandler<EventArgs> QueueProcessed;
+        public EventHandler<EventArgs> QueueProcessing;
+
+        private bool IsProcessed {
+            get {
+                bool ret = false;
+
+                lock (sync) {
+                    if (eventQueue.Count == 0) {
+                        ret = true;
+                    }
+                }
+
+                return ret;
+            }
+        }
+
+        public virtual WaitHandle Handle {
+            get {
+                return executingHandle;
+            }
+        }
+
+        public CommandQueue () : this (null)
+        {
+        }
+
+        public CommandQueue (object sync)
+        {
+            userSync = sync;
+
+            if (userSync == null) {
+                execCommand = delegate (ICommand command) {
+                    command.Execute ();
+                };
+            } else {
+                execCommand = delegate (ICommand command) {
+                    lock (userSync) {
+                        command.Execute ();
+                    }
+                };
+            }
+
+            eventQueue = new Queue<ICommand> ();
+            registeredHandle = ThreadPool.RegisterWaitForSingleObject (are, ProcessEventQueue, null, -1, false);
+        }
+
+        public virtual void Dispose ()
+        {
+            if (queueThread == Thread.CurrentThread) {
+                throw new InvalidOperationException ("Cannot call 'Dispose' from a command queue thread.");
+            }
+
+            executingHandle.WaitOne ();
+
+            if (SetDisposed ()) {
+                if (registeredHandle != null) {
+                    registeredHandle.Unregister (null);
+                    registeredHandle = null;
+                }
+
+                if (are != null) {
+                    are.Close ();
+                    are = null;
+                }
+
+                if (executingHandle != null) {
+                    executingHandle.Close ();
+                    executingHandle = null;
+                }
+
+                eventQueue = null;
+            }
+        }
+
+        public virtual bool Register (CommandDelegate d)
+        {
+            return Register (new CommandWrapper (d));
+        }
+
+        public virtual bool Register (ICommand command)
+        {
+            lock (sync) {
+                if (disposed) {
+                    return false;
+                }
+
+                return Register (command, true);
+            }
+        }
+
+        protected virtual bool Register (ICommand command, bool pumpQueue)
+        {
+            if (command == null) {
+                throw new ArgumentNullException ("command");
+            }
+
+            eventQueue.Enqueue (command);
+
+            if (!executing && pumpQueue) {
+                SetExecuting (true);
+            }
+
+            return true;
+        }
+
+        public virtual bool Register (IEnumerable<ICommand> commands)
+        {
+            if (commands == null) {
+                throw new ArgumentNullException ("commands");
+            }
+
+            lock (sync) {
+                if (disposed) {
+                    return false;
+                }
+
+                foreach (ICommand c in commands) {
+                    Register (c, false);
+                }
+
+                if (!executing) {
+                    SetExecuting (true);
+                }
+            }
+
+            return true;
+        }
+
+        protected virtual void SetExecuting (bool exec)
+        {
+            if (exec) {
+                executing = true;
+                are.Set ();
+                executingHandle.Reset ();
+            } else {
+                executing = false;
+                executingHandle.Set ();
+            }
+        }
+
+        protected virtual bool SetDisposed ()
+        {
+            bool ret = false;
+
+            lock (sync) {
+                if (!disposed) {
+                    ret = disposed = true;
+                }
+            }
+
+            return ret;
+        }
+
+        protected virtual void ProcessEventQueue (object state, bool timedOut)
+        {
+            ICommand e;
+            bool done = false;
+
+            queueThread = Thread.CurrentThread;
+
+            while (!done) {
+                RaiseEvent (QueueProcessing);
+
+                while (true) {
+                    lock (sync) {
+                        e = eventQueue.Dequeue ();
+                        if (disposed) {
+                            throw new InvalidOperationException (
+                                String.Format ("{0}:  tis' executing whilist disposed.", GetType ().Name)
+                            );
+                        }
+                    }
+
+                    if (e != null) {
+                        try {
+                            execCommand (e);
+                        } catch (Exception ex) {
+                            Console.WriteLine (ex.Message);
+                            Console.WriteLine (ex.StackTrace);
+                            //Hyena.Log.Exception (ex);
+                        }
+                    }
+
+                    if (IsProcessed) {
+                        RaiseEvent (QueueProcessed);
+                        done = true;
+                    }
+
+                    if (done) {
+                        lock (sync) {
+                            if (eventQueue.Count == 0) {
+                                SetExecuting (false);
+                            } else {
+                                done = false;
+                            }
+                        }
+
+                        break;
+                    }
+                }
+            }
+        }
+
+        private void RaiseEvent (EventHandler<EventArgs> eve)
+        {
+            if (eve != null) {
+                try {
+                    eve (this, new EventArgs ());
+                } catch (Exception ex) {
+                    Console.WriteLine (ex.Message);
+                    //Hyena.Log.Exception (ex);
+                }
+            }
+        }
+    }
+}
+
diff --git a/src/Libraries/Migo2/Migo2.Async/CommandQueue/CommandWrapper.cs b/src/Libraries/Migo2/Migo2.Async/CommandQueue/CommandWrapper.cs
new file mode 100644
index 0000000..35618f7
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/CommandQueue/CommandWrapper.cs
@@ -0,0 +1,50 @@
+//
+// CommandWrapper.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public class CommandWrapper : ICommand
+    {
+        protected readonly CommandDelegate d;
+
+        public CommandWrapper (CommandDelegate del)
+        {
+            if (del == null) {
+                throw new ArgumentNullException ("del");
+            }
+
+            this.d = del;
+        }
+
+
+        public void Execute ()
+        {
+            d ();
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/CommandQueue/EventWrapper.cs b/src/Libraries/Migo2/Migo2.Async/CommandQueue/EventWrapper.cs
new file mode 100644
index 0000000..c5e0f9a
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/CommandQueue/EventWrapper.cs
@@ -0,0 +1,56 @@
+//
+// EventWrapper.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public class EventWrapper<T> : ICommand where T : EventArgs
+    {
+        private readonly T e;
+        private readonly object sender;
+        private readonly EventHandler<T> handler;
+
+        public EventWrapper (EventHandler<T> handler, object sender, T e)
+        {
+            if (handler == null) {
+                throw new ArgumentNullException ("handler");
+            } else if (e == null) {
+                throw new ArgumentNullException ("e");
+            }
+
+            this.e = e;
+            this.sender = sender;
+            this.handler = handler;
+        }
+
+
+        public void Execute ()
+        {
+            handler (sender, e);
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/CommandQueue/ICommand.cs b/src/Libraries/Migo2/Migo2.Async/CommandQueue/ICommand.cs
new file mode 100644
index 0000000..d33fb6a
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/CommandQueue/ICommand.cs
@@ -0,0 +1,33 @@
+//
+// ICommand.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+namespace Migo2.Async
+{
+    public interface ICommand
+    {
+        void Execute ();
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/Task/CancellationType.cs b/src/Libraries/Migo2/Migo2.Async/Task/CancellationType.cs
new file mode 100644
index 0000000..54bc267
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/Task/CancellationType.cs
@@ -0,0 +1,38 @@
+//
+// CancellationType.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public enum CancellationType : int
+    {
+        None = 0,
+        Aborted, // Cancel task, remove persistent task data (if any), task is marked as completed, remove task from group. 
+        Paused,  // Cancel task, keep persistent task data (if any), leave task in group.
+        Stopped, // Cancel task, keep persistent task data (if any), task is marked as completed, remove task from group.
+    }
+}
\ No newline at end of file
diff --git a/src/Libraries/Migo2/Migo2.Async/Task/IWaitableTask.cs b/src/Libraries/Migo2/Migo2.Async/Task/IWaitableTask.cs
new file mode 100644
index 0000000..2a7322f
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/Task/IWaitableTask.cs
@@ -0,0 +1,36 @@
+//
+// IWaitableTask.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Threading;
+
+namespace Migo2.Async
+{
+    public interface IWaitableTask
+    {
+        WaitHandle WaitHandle { get; }
+    }
+}
\ No newline at end of file
diff --git a/src/Libraries/Migo2/Migo2.Async/Task/Task.cs b/src/Libraries/Migo2/Migo2.Async/Task/Task.cs
new file mode 100644
index 0000000..a8e1308
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/Task/Task.cs
@@ -0,0 +1,393 @@
+//
+// Task.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Threading;
+using System.ComponentModel;
+
+namespace Migo2.Async
+{
+    public abstract class Task
+    {
+        private bool busy;
+        private bool completed;
+        private TaskState state;
+
+        private int progress;
+        private string name;
+        private object userState;
+
+        private CancellationType cancellationType;
+        private CancellationType requestedCancellationType;
+
+        private Guid groupID;
+        private CommandQueue commandQueue;
+
+        private readonly object syncRoot = new object ();
+
+        public EventHandler<TaskEventArgs> Started;
+        public EventHandler<TaskCompletedEventArgs> Completed;
+
+        public EventHandler<TaskEventArgs> Updated; // General update, name, other properties for re-draw.
+
+        public EventHandler<TaskStateChangedEventArgs> StateChanged;
+        public EventHandler<ProgressChangedEventArgs>  ProgressChanged;
+
+        public bool IsBusy
+        {
+            get {
+                lock (syncRoot) {
+                    return busy;
+                }
+            }
+        }
+
+        public bool IsFinished
+        {
+            get {
+                lock (syncRoot) {
+                    return completed;
+                }
+            }
+        }
+
+        protected CancellationType RequestedCancellationType
+        {
+            get { return requestedCancellationType; }
+        }
+
+        public CommandQueue EventQueue
+        {
+            get {
+                return commandQueue;
+            }
+
+            set {
+                lock (syncRoot) {
+                    commandQueue = value;
+                }
+            }
+        }
+
+        public string Name
+        {
+            get {
+                return name;
+            }
+
+            set {
+                lock (syncRoot) {
+                    if (value != name) {
+                        name = value;
+                        OnUpdated ();
+                    }
+                }
+            }
+        }
+
+        public int Progress
+        {
+            get {
+                return progress;
+            }
+
+            protected set {
+                SetProgress (value);
+            }
+        }
+
+        public virtual TaskState State {
+            get {
+                lock (syncRoot) {
+                    return state;
+                }
+            }
+
+            protected set {
+                lock (syncRoot) {
+                    SetState (value);
+                }
+            }
+        }
+
+        public object SyncRoot
+        {
+            get {
+                return syncRoot;
+            }
+        }
+
+        public object UserState {
+            get {
+                return userState;
+            }
+        }
+
+        internal Guid GroupID {
+            get {
+                return groupID;
+            }
+
+            set {
+                groupID = value;
+            }
+        }
+
+        protected Task () : this (String.Empty, null)
+        {
+        }
+
+        protected Task (string name, object userState)
+        {
+            progress = 0;
+            GroupID  = Guid.Empty;
+            state    = TaskState.Ready;
+
+            this.name = name;
+            this.userState = userState;
+        }
+
+        public abstract void CancelAsync ();
+        public abstract void ExecuteAsync ();
+
+        public virtual void StopAsync ()
+        {
+            CancelAsync ();
+        }
+
+        public virtual void PauseAsync ()
+        {
+            throw new NotImplementedException ("PauseAsync");
+        }
+
+        public virtual void ResumeAsync ()
+        {
+            lock (syncRoot) {
+                if (SetResumeRequested ()) {
+                    SetState (TaskState.Ready);
+                }
+            }
+        }
+
+        public override string ToString ()
+        {
+            return !String.IsNullOrEmpty (name) ? name : GetType ().ToString ();
+        }
+
+        protected virtual bool SetBusy ()
+        {
+            lock (syncRoot) {
+                if (busy) {
+                    throw new InvalidOperationException ("Concurrent operations are not supported.");
+                } else if (completed) {
+                    throw new InvalidOperationException ("Task executed previously.");
+                } else if (requestedCancellationType == CancellationType.None) {
+                    busy = true;
+                    SetState (TaskState.Running);
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected virtual bool SetCompleted ()
+        {
+            return SetCompleted (false);
+        }
+
+        protected virtual bool SetCompleted (bool paused)
+        {
+            lock (syncRoot) {
+                if (!completed) {
+                    busy = false;
+
+                    if (!paused) {
+                        completed = true;
+                    }
+
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected virtual void SetProgress (int progress)
+        {
+            lock (syncRoot) {
+                if (progress < 0 || progress > 100) {
+                    throw new ArgumentOutOfRangeException ("progress");
+                } else if (this.progress != progress) {
+                    this.progress = progress;
+                    OnProgressChanged (progress);
+                }
+            }
+        }
+
+        protected virtual bool SetRequestedCancellationType (CancellationType type)
+        {
+            lock (syncRoot) {
+                if (requestedCancellationType == CancellationType.None ||
+                    requestedCancellationType == CancellationType.Paused) {
+                    requestedCancellationType = type;
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected virtual bool SetResumeRequested ()
+        {
+            lock (syncRoot) {
+                if (!completed && !busy && cancellationType == CancellationType.Paused) {
+                    cancellationType = CancellationType.None;
+                    requestedCancellationType = CancellationType.None;
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected virtual void SetState (TaskState state)
+        {
+            if (this.state != state) {
+                TaskState oldState = this.state;
+                this.state = state;
+                OnStateChanged (oldState, state);
+            }
+        }
+
+        protected virtual void OnProgressChanged (int progress)
+        {
+            OnProgressChanged (
+                new ProgressChangedEventArgs (progress, userState)
+            );
+        }
+
+        protected virtual void OnProgressChanged (ProgressChangedEventArgs e)
+        {
+            EventHandler<ProgressChangedEventArgs> handler = ProgressChanged;
+
+            if (handler != null) {
+                CommandQueue queue = commandQueue;
+
+                if (queue != null) {
+                    queue.Register (delegate { handler (this, e); });
+                } else {
+                    ThreadPool.QueueUserWorkItem (delegate { handler (this, e); });
+                }
+            }
+        }
+
+        protected virtual void OnUpdated ()
+        {
+            EventHandler<TaskEventArgs> handler = Updated;
+
+            if (handler != null) {
+                CommandQueue queue = commandQueue;
+
+                if (queue != null) {
+                    queue.Register (delegate { handler (this, new TaskEventArgs ()); });
+                } else {
+                    ThreadPool.QueueUserWorkItem (delegate { handler (this, new TaskEventArgs ()); });
+                }
+            }
+        }
+
+        protected virtual void OnStarted ()
+        {
+            EventHandler<TaskEventArgs> handler = Started;
+
+            if (handler != null) {
+                CommandQueue queue = commandQueue;
+
+                if (queue != null) {
+                    queue.Register (delegate { handler (this, new TaskEventArgs ()); });
+                } else {
+                    ThreadPool.QueueUserWorkItem (delegate { handler (this, new TaskEventArgs ()); });
+                }
+            }
+        }
+
+        protected virtual void OnStateChanged (TaskState oldState, TaskState newState)
+        {
+            CommandQueue queue = commandQueue;
+            EventHandler<TaskStateChangedEventArgs> handler = StateChanged;
+
+            if (handler != null) {
+                if (queue != null) {
+                    queue.Register (
+                        delegate { handler (this, new TaskStateChangedEventArgs (oldState, newState)); }
+                    );
+                } else {
+                    ThreadPool.QueueUserWorkItem (
+                        delegate { handler (this, new TaskStateChangedEventArgs (oldState, newState)); }
+                    );
+                }
+            }
+        }
+
+        protected virtual void OnTaskCompleted (Exception error, bool cancelled)
+        {
+            TaskCompletedEventArgs e = null;
+            EventHandler<TaskCompletedEventArgs> handler = Completed;
+
+            lock (syncRoot) {
+                cancellationType = (cancelled) ? requestedCancellationType : CancellationType.None;
+
+                if (error != null) {
+                    SetState (TaskState.Failed);
+                } else if (!cancelled) {
+                    SetState (TaskState.Succeeded);
+                } else {
+                    switch (cancellationType) {
+                    case CancellationType.Aborted:
+                        SetState (TaskState.Cancelled); break;
+                    case CancellationType.Paused:
+                        SetState (TaskState.Paused);    break;
+                    case CancellationType.Stopped:
+                        SetState (TaskState.Stopped);   break;
+                    }
+                }
+
+                e = new TaskCompletedEventArgs (error, state, userState);
+            }
+
+            if (handler != null) {
+                CommandQueue queue = commandQueue;
+
+                if (queue != null) {
+                    queue.Register (delegate { handler (this, e); });
+                } else {
+                    ThreadPool.QueueUserWorkItem (delegate { handler (this, e); });
+                }
+            }
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/Task/TaskCompletedEventArgs.cs b/src/Libraries/Migo2/Migo2.Async/Task/TaskCompletedEventArgs.cs
new file mode 100644
index 0000000..7b75db0
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/Task/TaskCompletedEventArgs.cs
@@ -0,0 +1,90 @@
+//
+// TaskCompletedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public class TaskCompletedEventArgs : EventArgs
+    {
+        private readonly TaskState state;
+        private readonly Exception error;
+        private readonly object userState;
+
+        public bool Cancelled {
+            get { return (state == TaskState.Cancelled || state == TaskState.Stopped); }
+        }
+
+        public TaskState State
+        {
+            get { return state; }
+        }
+
+        public Exception Error
+        {
+            get { return error; }
+        }
+
+        public object UserState {
+            get {
+                return userState;
+            }
+        }
+
+        public TaskCompletedEventArgs (Exception error, TaskState state)
+            : this (error, state, null)
+        {
+        }
+
+        public TaskCompletedEventArgs (Exception error, TaskState state, object userState)
+        {
+            this.error = error;
+            this.state = state;
+            this.userState = userState;
+        }
+    }
+
+    public class TaskCompletedEventArgs<T> : TaskCompletedEventArgs where T : Task
+    {
+        private readonly T task;
+
+        public T Task {
+            get { return task; }
+        }
+
+        public TaskCompletedEventArgs (T task, TaskCompletedEventArgs args)
+            : base (args.Error, args.State, args.UserState)
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            } else if (args == null) {
+                throw new ArgumentNullException ("args");
+            }
+
+            this.task = task;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/Task/TaskEventArgs.cs b/src/Libraries/Migo2/Migo2.Async/Task/TaskEventArgs.cs
new file mode 100644
index 0000000..fab768d
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/Task/TaskEventArgs.cs
@@ -0,0 +1,56 @@
+//
+// TaskEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public class TaskEventArgs : EventArgs
+    {
+        public TaskEventArgs ()
+        {
+        }
+    }
+
+    public class TaskEventArgs<T> : TaskEventArgs where T : Task
+    {
+        private readonly T task;
+
+        public T Task {
+            get { return task; }
+        }
+
+        public TaskEventArgs (T task)
+            : base ()
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            }
+
+            this.task = task;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/Task/TaskState.cs b/src/Libraries/Migo2/Migo2.Async/Task/TaskState.cs
new file mode 100644
index 0000000..63359a5
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/Task/TaskState.cs
@@ -0,0 +1,46 @@
+//
+// TaskState.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    [Flags]
+    public enum TaskState
+    {
+        Zero      = 0x0000,
+        None      = 0x0001,
+        Ready     = 0x0002,
+        Running   = 0x0004,
+        Paused    = 0x0008,
+        Stopped   = 0x0016,
+        Failed    = 0x0032,
+        Cancelled = 0x0064,
+        Succeeded = 0x0128,
+        CanCancel = (Ready | Running | Paused),
+        CanPause  = (Ready | Running)
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/Task/TaskStateChangedEventArgs.cs b/src/Libraries/Migo2/Migo2.Async/Task/TaskStateChangedEventArgs.cs
new file mode 100644
index 0000000..800be91
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/Task/TaskStateChangedEventArgs.cs
@@ -0,0 +1,73 @@
+//
+// TaskStateChangedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public class TaskStateChangedEventArgs : EventArgs
+    {
+        private readonly TaskState old_state;
+        private readonly TaskState new_state;
+
+        public TaskState OldState
+        {
+            get { return old_state; }
+        }
+
+        public TaskState NewState
+        {
+            get { return new_state; }
+        }
+
+        public TaskStateChangedEventArgs (TaskState oldState, TaskState newState)
+        {
+            old_state = oldState;
+            new_state = newState;
+        }
+    }
+
+    public class TaskStateChangedEventArgs<T> : TaskStateChangedEventArgs where T : Task
+    {
+        private readonly T task;
+
+        public T Task {
+            get { return task; }
+        }
+
+        public TaskStateChangedEventArgs (T task, TaskStateChangedEventArgs args)
+            : base (args.OldState, args.NewState)
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            } else if (args == null) {
+                throw new ArgumentNullException ("args");
+            }
+
+            this.task = task;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/GroupStatusChangedEventArgs.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/GroupStatusChangedEventArgs.cs
new file mode 100644
index 0000000..a614f25
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/GroupStatusChangedEventArgs.cs
@@ -0,0 +1,58 @@
+//
+// GroupStatusChangedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public class GroupStatusChangedEventArgs : EventArgs
+    {
+        private readonly int remainingTasks;
+        private readonly int runningTasks;
+        private readonly int completedTasks;
+
+        public int RemainingTasks {
+            get { return remainingTasks; }
+        }
+
+        public int RunningTasks {
+            get { return runningTasks; }
+        }
+
+        public int CompletedTasks {
+            get { return completedTasks; }
+        }
+
+        public GroupStatusChangedEventArgs (int remainingTasks,
+                                            int runningTasks,
+                                            int completedTasks)
+        {
+            this.remainingTasks = remainingTasks;
+            this.runningTasks = runningTasks;
+            this.completedTasks = completedTasks;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/ManipulatedEventArgs.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/ManipulatedEventArgs.cs
new file mode 100644
index 0000000..e54a8dd
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/ManipulatedEventArgs.cs
@@ -0,0 +1,78 @@
+//
+// ManipulatedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+namespace Migo2.Async
+{
+    public class ManipulatedEventArgs<T> : EventArgs
+    {
+        private T task;
+        private ICollection<T> tasks;
+
+        public T Task {
+            get { return task; }
+
+            protected internal set {
+                TestCombination (value, tasks);
+                task = value;
+            }
+        }
+
+        public ICollection<T> Tasks {
+            get { return tasks; }
+
+            protected internal set {
+                TestCombination (task, value);
+                tasks = value;
+            }
+        }
+
+        protected internal ManipulatedEventArgs () {}
+
+        protected ManipulatedEventArgs (T task, ICollection<T> tasks)
+        {
+            TestCombination (task, tasks);
+            this.task = task;
+            this.tasks = tasks;
+        }
+
+        private void TestCombination (T task, ICollection<T> tasks)
+        {
+            if (task != null && tasks != null) {
+                throw new ArgumentException (
+                    "Either task or tasks must be null"
+                );
+            } else if (task == null && tasks == null) {
+                throw new InvalidOperationException (
+                    "Both task and tasks may not be null"
+                );
+            }
+        }
+    }
+}
+
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/ReorderedEventArgs.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/ReorderedEventArgs.cs
new file mode 100644
index 0000000..1d6c7be
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/ReorderedEventArgs.cs
@@ -0,0 +1,46 @@
+//
+// ReorderedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Async
+{
+    public class ReorderedEventArgs : EventArgs
+    {
+        private readonly int[] order;
+
+        public int[] NewOrder
+        {
+            get { return order; }
+        }
+
+        // order[i] = old position of element previously located at position i;
+        public ReorderedEventArgs (int[] order)
+        {
+            this.order = order;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/TaskAddedEventArgs.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/TaskAddedEventArgs.cs
new file mode 100644
index 0000000..f53888b
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/TaskAddedEventArgs.cs
@@ -0,0 +1,81 @@
+//
+// TaskAddedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Migo2.Collections;
+
+namespace Migo2.Async
+{
+    public class TaskAddedEventArgs<T> : ManipulatedEventArgs<T> where T : Task
+    {
+        private readonly Pair<int,T> taskPair;
+        private readonly ICollection<Pair<int,T>> taskPairs;
+
+        public Pair<int,T> TaskPair
+        {
+            get { return taskPair; }
+        }
+
+        // All indices are listed in ascending order from the start of the
+        // list so that in order addition will not affect indices.
+        public ICollection<Pair<int,T>> TaskPairs
+        {
+            get { return taskPairs; }
+        }
+
+        public TaskAddedEventArgs (int pos, T task) : base (task, null)
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            }
+
+            this.taskPair = new Pair<int,T> (pos, task);
+        }
+
+        public TaskAddedEventArgs (ICollection<Pair<int,T>> taskPairs)
+        {
+            if (taskPairs == null) {
+                throw new ArgumentNullException ("taskPairs");
+            }
+
+            List<T> tsks = new List<T> (taskPairs.Count);
+
+            foreach (Pair<int,T> kvp in taskPairs) {
+                if (kvp.Second == null) {
+                    throw new ArgumentNullException ("No task in tasks may be null");
+                }
+
+                tsks.Add (kvp.Second);
+            }
+
+            this.Tasks = tsks;
+            this.taskPairs = taskPairs;
+            this.taskPair = default (Pair<int,T>);
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/TaskProgressChangedEventArgs.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/TaskProgressChangedEventArgs.cs
new file mode 100644
index 0000000..fff5b93
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/TaskProgressChangedEventArgs.cs
@@ -0,0 +1,54 @@
+//
+// TaskProgressChangedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.ComponentModel;
+
+namespace Migo2.Async
+{
+    public class TaskProgressChangedEventArgs<T> : ProgressChangedEventArgs where T : Task
+    {
+        private readonly T task;
+
+        public T Task
+        {
+            get { return task; }
+        }
+
+        public TaskProgressChangedEventArgs (T task, int progress) : this (task, progress, null)
+        {
+        }
+
+        public TaskProgressChangedEventArgs (T task, int progress, object userState) : base (progress, userState)
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            }
+
+            this.task = task;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/TaskRemovedEventArgs.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/TaskRemovedEventArgs.cs
new file mode 100644
index 0000000..1316f1d
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/EventArgs/TaskRemovedEventArgs.cs
@@ -0,0 +1,79 @@
+//
+// TaskRemovedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Migo2.Collections;
+
+namespace Migo2.Async
+{
+    public class TaskRemovedEventArgs<T> : ManipulatedEventArgs<T> where T : Task
+    {
+        private readonly int index;
+        private readonly IEnumerable<Pair<int,int>> indices;
+
+        public int Index
+        {
+            get { return index; }
+        }
+
+        // All indices are listed in descending order from the end of the
+        // list so that in order removal will not affect indices.
+
+        // int - 0:  Index
+        // int - 1:  Count
+        public IEnumerable <Pair<int,int>> Indices
+        {
+            get { return indices; }
+        }
+
+        protected TaskRemovedEventArgs (int index, T task,
+                                        IEnumerable<Pair<int,int>> indices,
+                                        ICollection<T> tasks)
+            : base (task, tasks)
+        {
+            if (indices == null && index < 0) {
+                throw new InvalidOperationException (
+                    "indices may not be null if index is < 0"
+                );
+            }
+
+            this.index = index;
+            this.indices = indices;
+        }
+
+        public TaskRemovedEventArgs (int index, T task)
+            : this (index, task, null, null)
+        {
+        }
+
+        public TaskRemovedEventArgs (IEnumerable<Pair<int,int>> indices, ICollection<T> tasks)
+            : this (-1, null, indices, tasks)
+        {
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/GroupProgressManager.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/GroupProgressManager.cs
new file mode 100644
index 0000000..285ce4e
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/GroupProgressManager.cs
@@ -0,0 +1,176 @@
+//
+// GroupProgressManager.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.ComponentModel;
+using System.Collections.Generic;
+
+namespace Migo2.Async
+{
+    public class GroupProgressManager<T> where T : Task
+    {
+        private int progress;
+        private int oldProgress;
+
+        private int totalTicks;
+        private int currentTicks;
+
+        private Dictionary<T, int> progDict;
+
+        public event EventHandler<ProgressChangedEventArgs> ProgressChanged;
+
+        public GroupProgressManager ()
+        {
+            progDict = new Dictionary<T, int> ();
+        }
+
+        public virtual void Add (T task)
+        {
+            Add (task, true);
+        }
+
+        public virtual void Add (IEnumerable<T> tasks)
+        {
+            foreach (T task in tasks) {
+                Add (task, false);
+            }
+
+            OnProgressChanged ();
+        }
+
+        protected virtual void Add (T task, bool update)
+        {
+            if (progDict.ContainsKey (task)) {
+                throw new ArgumentException ("Task was added previously");
+            }
+
+            totalTicks += 100;
+            progDict.Add (task, 0);
+
+            if (update) {
+                OnProgressChanged ();
+            }
+        }
+
+        public virtual void Remove (T task)
+        {
+            Remove (task, true);
+        }
+
+        public virtual void Remove (IEnumerable<T> tasks)
+        {
+            foreach (T task in tasks) {
+                try {
+                    Remove (task, false);
+                } catch { continue; }
+            }
+
+            OnProgressChanged ();
+        }
+
+        protected virtual void Remove (T task, bool update)
+        {
+            if (task.Progress == 100) {
+                if (progDict.ContainsKey (task)) {
+                    progDict.Remove (task);
+                }
+            } else {
+                int prog = 0;
+
+                if (progDict.ContainsKey (task)) {
+                    prog = progDict[task];
+                    progDict.Remove (task);
+
+                    currentTicks -= prog;
+                    totalTicks -= 100;
+                }
+
+                if (update) {
+                    OnProgressChanged ();
+                }
+            }
+        }
+
+        public virtual void Reset ()
+        {
+            progress = 0;
+            oldProgress = 0;
+
+            totalTicks = 0;
+            currentTicks = 0;
+
+            progDict.Clear ();
+        }
+
+        public virtual void Update (T task, int newProg)
+        {
+            if (newProg < 0) {
+                throw new ArgumentOutOfRangeException (
+                    "newProg must be greater than or equal to 0"
+                );
+            }
+
+            int delta = 0;
+
+            if (progDict.ContainsKey (task)) {
+                int prog = progDict[task];
+
+                if (prog != newProg) {
+                    progDict[task] = newProg;
+                    delta = newProg - prog;
+                }
+            }
+
+            if (delta != 0) {
+                currentTicks += delta;
+                OnProgressChanged ();
+            }
+        }
+
+        protected virtual void OnProgressChanged ()
+        {
+            if (totalTicks == 0) {
+                progress = 0;
+            } else {
+                progress = Convert.ToInt32 (
+                    (currentTicks * 100) / totalTicks
+                );
+            }
+
+            if (progress != oldProgress) {
+                oldProgress = progress;
+
+                EventHandler<ProgressChangedEventArgs> handler = ProgressChanged;
+
+                if (handler != null) {
+                    handler (
+                        this, new ProgressChangedEventArgs (progress, null)
+                    );
+                }
+            }
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/GroupStatusManager.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/GroupStatusManager.cs
new file mode 100644
index 0000000..3f85430
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/GroupStatusManager.cs
@@ -0,0 +1,349 @@
+//
+// GroupStatusManager.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Threading;
+using System.Collections.Generic;
+
+namespace Migo2.Async
+{
+    public class GroupStatusManager : IDisposable
+    {
+        private bool disposed;
+        private bool suspendUpdate;
+
+        private int runningTasks;
+        private int completedTasks;
+        private int remainingTasks;
+        private int maxRunningTasks = 0;
+
+        private ManualResetEvent mre;
+        private List<Task> pendingTasks; // Should be a Dictionary
+
+        public event EventHandler<GroupStatusChangedEventArgs> StatusChanged;
+
+        public virtual int CompletedTasks
+        {
+            get {
+                CheckDisposed ();
+                return completedTasks;
+            }
+        }
+
+        public virtual int RunningTasks
+        {
+            get {
+                CheckDisposed ();
+                return runningTasks;
+            }
+        }
+
+        public virtual bool SuspendUpdate
+        {
+            get {
+                CheckDisposed ();
+                return suspendUpdate;
+            }
+
+            set {
+                CheckDisposed ();
+                suspendUpdate = value;
+            }
+        }
+
+        public virtual int RemainingTasks
+        {
+            get {
+                CheckDisposed ();
+                return remainingTasks;
+            }
+
+            set {
+                CheckDisposed ();
+                SetRemainingTasks (value);
+            }
+        }
+
+        public virtual int MaxRunningTasks
+        {
+            get {
+                CheckDisposed ();
+                return maxRunningTasks;
+            }
+
+            set {
+                CheckDisposed ();
+                SetMaxRunningTasks (value);
+            }
+        }
+
+        public virtual WaitHandle Handle {
+            get {
+                CheckDisposed ();
+                return mre;
+            }
+        }
+
+        public GroupStatusManager () : this (0, 0)
+        {
+        }
+
+        public GroupStatusManager (int totalTasks) : this (totalTasks, 0)
+        {
+        }
+
+        public GroupStatusManager (int maxRunningTasks, int totalTasks)
+        {
+            pendingTasks = new List<Task> (maxRunningTasks);
+            mre = new ManualResetEvent (false);
+
+            SetRemainingTasks (totalTasks);
+            SetMaxRunningTasks (maxRunningTasks);
+        }
+
+        public virtual void Dispose ()
+        {
+            if (!disposed) {
+                disposed = true;
+
+                if (mre != null) {
+                    mre.Close ();
+                    mre = null;
+                }
+            }
+        }
+
+        public virtual bool DropPendingTask (Task task)
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            }
+
+            return pendingTasks.Remove (task);
+        }
+
+        public virtual bool RegisterPendingTask (Task task)
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            }
+
+            if (!pendingTasks.Contains (task)) {
+                pendingTasks.Add (task);
+                return true;
+            }
+
+            return false;
+        }
+
+        public virtual int IncrementCompletedTaskCount ()
+        {
+            CheckDisposed ();
+
+            ++completedTasks;
+            OnStatusChanged ();
+
+            return completedTasks;
+        }
+
+        public virtual int DecrementCompletedTaskCount ()
+        {
+            CheckDisposed ();
+
+            if (completedTasks == 0) {
+                throw new InvalidOperationException ("Completed task count cannot be less than 0");
+            }
+
+            --completedTasks;
+            OnStatusChanged ();
+
+            return completedTasks;
+        }
+
+        public virtual int IncrementRunningTaskCount ()
+        {
+            CheckDisposed ();
+
+            if (runningTasks >= remainingTasks) {
+                throw new InvalidOperationException ("Running task count cannot be > remaining task count");
+            }
+
+            ++runningTasks;
+            OnStatusChanged ();
+
+            Evaluate ();
+
+            return runningTasks;
+        }
+
+        public virtual int DecrementRunningTaskCount ()
+        {
+            CheckDisposed ();
+
+            if (runningTasks == 0) {
+                throw new InvalidOperationException ("Runing task count cannot be less than 0");
+            }
+
+            --runningTasks;
+            OnStatusChanged ();
+            Evaluate ();
+
+            return runningTasks;
+        }
+
+        public virtual int IncrementRemainingTaskCount ()
+        {
+            CheckDisposed ();
+
+            SetRemainingTasks (remainingTasks + 1);
+            Evaluate ();
+
+            return remainingTasks;
+        }
+
+        public virtual int DecrementRemainingTaskCount ()
+        {
+            CheckDisposed ();
+
+            if (remainingTasks == 0) {
+                throw new InvalidOperationException ("Remaining task count cannot be less than 0");
+            }
+
+            SetRemainingTasks (remainingTasks - 1);
+            Evaluate ();
+
+            return remainingTasks;
+        }
+
+        public virtual void ResetWait ()
+        {
+            CheckDisposed ();
+            mre.Reset ();
+        }
+
+        public virtual void Reset ()
+        {
+            completedTasks = 0;
+            suspendUpdate = false;
+        }
+
+        public virtual bool SetRemainingTasks (int newRemainingTasks)
+        {
+            CheckDisposed ();
+            if (newRemainingTasks < 0) {
+                throw new ArgumentException ("newRemainingTasks must be >= 0");
+            }
+
+            bool ret = false;
+
+            if (remainingTasks != newRemainingTasks) {
+                ret = true;
+                remainingTasks = newRemainingTasks;
+                Evaluate ();
+                OnStatusChanged ();
+            }
+
+            return ret;
+        }
+
+        public virtual void Update ()
+        {
+            CheckDisposed ();
+            OnStatusChanged ();
+            Evaluate ();
+        }
+
+        public virtual void Wait ()
+        {
+            CheckDisposed ();
+            mre.WaitOne ();
+        }
+
+        protected virtual void CheckDisposed ()
+        {
+            if (disposed) {
+                throw new ObjectDisposedException (GetType ().FullName);
+            }
+        }
+
+        public virtual void Evaluate ()
+        {
+            if (suspendUpdate) {
+                return;
+            }
+/*
+            if ((remainingTasks == 0 && maxRunningTasks > 0) ||
+                (runningTasks < maxRunningTasks && (remainingTasks - runningTasks) > 0)) {
+                mre.Set ();
+            }
+*/
+            int pendingAndRunningTasks = pendingTasks.Count + runningTasks;
+
+            if ((remainingTasks == 0 && maxRunningTasks > 0) ||
+                (pendingAndRunningTasks < maxRunningTasks && (remainingTasks - pendingAndRunningTasks) > 0)) {
+                mre.Set ();
+            }
+        }
+
+        protected virtual void SetMaxRunningTasks (int newMaxTasks)
+        {
+            CheckDisposed ();
+
+            if (newMaxTasks < 0) {
+                throw new ArgumentException ("newMaxTasks must be >= 0");
+            }
+
+            if (maxRunningTasks != newMaxTasks) {
+                maxRunningTasks = newMaxTasks;
+                Evaluate ();
+            }
+        }
+
+        protected virtual void OnStatusChanged (GroupStatusChangedEventArgs e)
+        {
+            if (suspendUpdate) {
+                return;
+            }
+
+            EventHandler<GroupStatusChangedEventArgs> handler = StatusChanged;
+
+            if (handler != null) {
+                handler (this, e);
+            }
+        }
+
+        protected virtual void OnStatusChanged ()
+        {
+            if (suspendUpdate) {
+                return;
+            }
+
+            OnStatusChanged (
+                new GroupStatusChangedEventArgs (remainingTasks, runningTasks, completedTasks)
+            );
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/TaskGroup.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/TaskGroup.cs
new file mode 100644
index 0000000..04b1478
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/TaskGroup.cs
@@ -0,0 +1,690 @@
+//
+// TaskGroup.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Threading;
+using System.ComponentModel;
+
+using System.Collections;
+using System.Collections.Generic;
+
+using Migo2.Collections;
+
+namespace Migo2.Async
+{
+    public partial class TaskGroup<T> where T : Task
+    {
+        private bool disposed;
+        private bool is_disposing;
+
+        private bool executing;
+        private bool cancelRequested;
+
+        private List<T> currentTasks;
+        private CommandQueue commandQueue;
+
+        private GroupStatusManager gsm;
+        private GroupProgressManager<T> gpm;
+
+        private ManualResetEvent mre;
+
+        private readonly Guid id;
+        private readonly object sync = new object ();
+
+        public event EventHandler<TaskEventArgs<T>> TaskStarted;
+        public event EventHandler<TaskCompletedEventArgs<T>> TaskCompleted;
+
+        public event EventHandler<TaskEventArgs<T>> TaskUpdated;
+        public event EventHandler<TaskStateChangedEventArgs<T>> TaskStateChanged;
+        public event EventHandler<TaskProgressChangedEventArgs<T>> TaskProgressChanged;
+
+        public event EventHandler<EventArgs> Started;
+        public event EventHandler<EventArgs> Stopped;
+        public event EventHandler<ProgressChangedEventArgs> ProgressChanged;
+        public event EventHandler<GroupStatusChangedEventArgs> StatusChanged;
+
+        public virtual int CompletedTasks
+        {
+            get {
+                lock (sync) {
+                    CheckDisposed ();
+                    return gsm.CompletedTasks;
+                }
+            }
+        }
+
+        protected virtual bool IsDisposed
+        {
+            get {
+                lock (sync) { return disposed; }
+            }
+        }
+
+        protected virtual bool IsDisposing
+        {
+            get {
+                lock (sync) { return disposed | is_disposing; }
+            }
+        }
+
+        public virtual bool IsBusy
+        {
+            get {
+                lock (sync) {
+                    CheckDisposed ();
+                    return executing;
+                }
+            }
+        }
+
+        public virtual int RunningTasks
+        {
+            get {
+                lock (sync) {
+                    CheckDisposed ();
+                    return gsm.RunningTasks;
+                }
+            }
+        }
+
+        public virtual int RemainingTasks
+        {
+            get {
+                lock (sync) {
+                    CheckDisposed ();
+                    return gsm.RemainingTasks;
+                }
+            }
+        }
+
+        public WaitHandle Handle {
+            get { return mre; }
+        }
+
+        protected CommandQueue EventQueue
+        {
+            get { return commandQueue; }
+        }
+
+        protected List<T> CurrentTasks {
+            get { return currentTasks; }
+        }
+
+        protected GroupProgressManager<T> ProgressManager
+        {
+            get { return gpm; }
+            set { SetProgressManager (value); }
+        }
+
+        protected GroupStatusManager StatusManager
+        {
+            get {
+                if (gsm == null) {
+                    SetStatusManager (new GroupStatusManager ());
+                }
+
+                return gsm;
+            }
+
+            set { SetStatusManager (value); }
+        }
+
+        public int MaxRunningTasks
+        {
+            get {
+                lock (sync) {
+                    CheckDisposed ();
+                    return gsm.MaxRunningTasks;
+                }
+            }
+
+            set {
+                lock (sync) {
+                    CheckDisposed ();
+                    gsm.MaxRunningTasks = value;
+                }
+            }
+        }
+
+        public object SyncRoot
+        {
+            get { return sync; }
+        }
+
+        private bool IsDone
+        {
+            get { return (gsm.RemainingTasks == 0); }
+        }
+
+        public TaskGroup (int maxRunningTasks)
+            : this (maxRunningTasks, null, null)
+        {
+        }
+
+        protected TaskGroup (int maxRunningTasks, GroupStatusManager statusManager)
+            : this (maxRunningTasks, statusManager, null)
+        {
+        }
+
+        protected TaskGroup (int maxRunningTasks,
+                             GroupStatusManager statusManager,
+                             GroupProgressManager<T> progressManager)
+        {
+            if (maxRunningTasks < 0) {
+                throw new ArgumentException ("maxRunningTasks must be >= 0");
+            }
+
+            mre = new ManualResetEvent (true);
+
+            InitTaskGroup_Collection ();
+            currentTasks = new List<T> (maxRunningTasks);
+
+            id = Guid.NewGuid ();
+            commandQueue = new CommandQueue ();
+
+            SetStatusManager (statusManager ?? new GroupStatusManager ());
+            SetProgressManager (progressManager ?? new GroupProgressManager<T> ());
+
+            gsm.MaxRunningTasks = maxRunningTasks;
+        }
+
+        public virtual void CancelAsync ()
+        {
+            if (SetCancelled ()) {
+                lock (sync) {
+                    foreach (T task in list) {
+                        task.CancelAsync ();
+                    }
+                }
+            }
+        }
+
+        public virtual void StopAsync ()
+        {
+            if (SetCancelled ()) {
+                lock (sync) {
+                    foreach (T task in list) {
+                        task.StopAsync ();
+                    }
+                }
+            }
+        }
+
+        public virtual void Dispose ()
+        {
+            if (SetDisposing ()) {
+                commandQueue.Dispose ();
+
+                if (SetDisposed ()) {
+                    DisposeImpl ();
+                }
+            }
+        }
+
+        // Most subclasses will only need to override DisposeImpl
+        protected virtual void DisposeImpl ()
+        {
+            gsm.StatusChanged -= OnStatusChangedHandler;
+            gpm.ProgressChanged -= OnProgressChangedHandler;
+
+            if (gsm is IDisposable) {
+                gsm.Dispose ();
+            }
+
+            mre.Close ();
+
+            gpm = null;
+            gsm = null;
+            mre = null;
+        }
+
+        public void Execute ()
+        {
+            if (SetExecuting (true)) {
+                OnStarted ();
+                SpawnExecutionThread ();
+            }
+        }
+
+        protected virtual bool SetCancelled ()
+        {
+            lock (sync) {
+                CheckDisposed ();
+
+                if (executing && !cancelRequested) {
+                    cancelRequested = true;
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected bool SetDisposed ()
+        {
+            lock (sync) {
+                if (executing) {
+                    throw new InvalidOperationException ("Unable to dispose while executing");
+                }
+
+                if (!disposed) {
+                    disposed = true;
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected bool SetDisposing ()
+        {
+            lock (SyncRoot) {
+                // It's ok for sub-classes to call this in their dispose methods too.
+                // Thank you past man, you saved me a lot of time.
+                if (!disposed) {
+                    is_disposing = true;
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected virtual bool SetExecuting (bool exec)
+        {
+            lock (sync) {
+                CheckDisposed ();
+
+                if (exec) {
+                    if (!executing && !cancelRequested) {
+                        executing = true;
+                        return true;
+                    }
+                } else {
+                    executing = false;
+                    cancelRequested = false;
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        protected virtual void SetProgressManager (GroupProgressManager<T> progressManager)
+        {
+            CheckDisposed ();
+
+            if (progressManager == null) {
+                throw new ArgumentNullException ("progressManager");
+            } else if (gpm != null) {
+                throw new InvalidOperationException ("ProgressManager already set");
+            }
+
+            gpm = progressManager;
+            gpm.ProgressChanged += OnProgressChangedHandler;
+        }
+
+        protected virtual void SetStatusManager (GroupStatusManager statusManager)
+        {
+            CheckDisposed ();
+
+            if (statusManager == null) {
+                throw new ArgumentNullException ("statusManager");
+            } else if (gsm != null) {
+                throw new InvalidOperationException ("StatusManager already set");
+            }
+
+            gsm = statusManager;
+            gsm.StatusChanged += OnStatusChangedHandler;
+        }
+
+        protected virtual void CheckDisposed ()
+        {
+            if (disposed) {
+                throw new ObjectDisposedException (GetType ().FullName);
+            }
+        }
+
+        protected virtual void Associate (T task)
+        {
+            Associate (task, true);
+        }
+
+        protected virtual void Associate (IEnumerable<T> tasks)
+        {
+            foreach (T task in tasks) {
+                if (task != null) {
+                    Associate (task, false);
+                }
+            }
+
+            gpm.Add (tasks);
+        }
+
+        protected virtual void Associate (T task, bool addToProgressGroup)
+        {
+            CheckDisposed ();
+
+            if (task.GroupID != Guid.Empty) {
+                throw new ApplicationException ("task:  Already associated with a group");
+            }
+
+            task.GroupID = id;
+            task.EventQueue = commandQueue;
+
+            task.Started += OnTaskStartedHandler;
+            task.Completed += OnTaskCompletedHandler;
+
+            task.Updated += OnTaskUpdatedHandler;
+            task.StateChanged += OnTaskStateChangedHandler;
+            task.ProgressChanged += OnTaskProgressChangedHandler;
+
+            if (addToProgressGroup) {
+                gpm.Add (task);
+            }
+        }
+
+        protected virtual bool CheckID (T task)
+        {
+            return (task.GroupID == id);
+        }
+
+        protected virtual void Disassociate (IEnumerable<T> tasks)
+        {
+            foreach (T task in tasks) {
+                Disassociate (task, false);
+            }
+
+            gpm.Remove (tasks);
+        }
+
+        protected virtual void Disassociate (T task)
+        {
+            Disassociate (task, true);
+        }
+
+        protected virtual void Disassociate (T task, bool removeFromGroup)
+        {
+            if (CheckID (task)) {
+                task.EventQueue = null;
+                task.GroupID = Guid.Empty;
+
+                task.Started -= OnTaskStartedHandler;
+                task.Completed -= OnTaskCompletedHandler;
+
+                task.Updated -= OnTaskUpdatedHandler;
+                task.StateChanged -= OnTaskStateChangedHandler;
+                task.ProgressChanged -= OnTaskProgressChangedHandler;
+
+                if (removeFromGroup) {
+                    list.Remove (task);
+                    gpm.Remove (task);
+                }
+            }
+        }
+
+        protected virtual void Reset ()
+        {
+            lock (sync) {
+                gsm.Reset ();
+                gpm.Reset ();
+            }
+        }
+
+        protected virtual void OnStarted ()
+        {
+            OnTaskGroupStarted ();
+            mre.Reset ();
+        }
+
+        protected virtual void OnStopped ()
+        {
+            OnTaskGroupStopped ();
+            mre.Set ();
+        }
+
+        protected virtual void OnTaskStarted (T task)
+        {
+            OnTaskEvent (task, TaskStarted);
+        }
+
+        protected virtual void OnTaskCompleted (T task, TaskCompletedEventArgs e)
+        {
+            EventHandler<TaskCompletedEventArgs<T>> handler = TaskCompleted;
+
+            if (handler != null) {
+                commandQueue.Register (
+                    new EventWrapper<TaskCompletedEventArgs<T>> (
+                        handler, this, new TaskCompletedEventArgs<T> (task, e)
+                    )
+                );
+            }
+        }
+
+        protected virtual void OnTaskStateChanged (T task, TaskStateChangedEventArgs e)
+        {
+            EventHandler<TaskStateChangedEventArgs<T>> handler = TaskStateChanged;
+
+            if (handler != null) {
+                commandQueue.Register (
+                    new EventWrapper<TaskStateChangedEventArgs<T>> (
+                        handler, this, new TaskStateChangedEventArgs<T> (task, e)
+                    )
+                );
+            }
+        }
+
+        protected virtual void OnTaskStateChangedHandler (object sender, TaskStateChangedEventArgs e)
+        {
+            lock (sync) {
+                if (e.OldState == TaskState.Paused && e.NewState == TaskState.Ready) {
+                    gsm.Evaluate ();
+                }
+
+                OnTaskStateChanged (sender as T, e);
+            }
+        }
+
+        protected virtual void OnTaskUpdatedHandler (object sender, TaskEventArgs e)
+        {
+            OnTaskEvent (sender as T, TaskUpdated);
+        }
+
+        protected virtual void OnStatusChangedHandler (object sender, GroupStatusChangedEventArgs e)
+        {
+            lock (sync) {
+                EventHandler<GroupStatusChangedEventArgs> handler = StatusChanged;
+
+                if (handler != null) {
+                    commandQueue.Register (new EventWrapper<GroupStatusChangedEventArgs> (handler, this, e));
+                }
+            }
+        }
+
+        protected virtual void OnTaskCompletedHandler (object sender, TaskCompletedEventArgs e)
+        {
+            lock (sync) {
+                T t = sender as T;
+                gsm.SuspendUpdate = true;
+
+                try {
+                    gsm.DropPendingTask (t);
+
+                    if (currentTasks.Contains (t)) {
+                        currentTasks.Remove (t);
+
+                        if (e.State != TaskState.Paused && !e.Cancelled) {
+                            gsm.IncrementCompletedTaskCount ();
+                        }
+
+                        gsm.DecrementRunningTaskCount ();
+                    }
+                } finally {
+                    if (e.State != TaskState.Paused) {
+                        gsm.DecrementRemainingTaskCount ();
+                        Disassociate (t);
+                    }
+
+                    OnTaskCompleted (t, e);
+
+                    gsm.SuspendUpdate = false;
+                    gsm.Update ();
+                }
+            }
+        }
+
+        protected virtual void OnTaskProgressChangedHandler (object sender, ProgressChangedEventArgs e)
+        {
+            EventHandler<TaskProgressChangedEventArgs<T>> handler = TaskProgressChanged;
+
+            lock (sync) {
+                gpm.Update (sender as T, e.ProgressPercentage);
+            }
+
+            if (handler != null) {
+                handler (this, new TaskProgressChangedEventArgs<T> (sender as T, e.ProgressPercentage, e.UserState));
+            }
+        }
+
+        protected virtual void OnTaskStartedHandler (object sender, EventArgs e)
+        {
+            lock (sync) {
+                T task = sender as T;
+
+                currentTasks.Add (task);
+
+                gsm.DropPendingTask (task);
+                gsm.IncrementRunningTaskCount ();
+
+                OnTaskStarted (task);
+            }
+        }
+
+        protected virtual void OnProgressChangedHandler (object sender, ProgressChangedEventArgs e)
+        {
+            lock (sync) {
+                EventHandler<ProgressChangedEventArgs> handler = ProgressChanged;
+
+                if (handler != null) {
+                    commandQueue.Register (
+                        new EventWrapper<ProgressChangedEventArgs> (handler, this, e)
+                    );
+                }
+            }
+        }
+
+        private void OnTaskGroupStarted ()
+        {
+            EventHandler<EventArgs> handler = Started;
+
+            if (handler != null) {
+                commandQueue.Register (
+                    new EventWrapper<EventArgs> (handler, this, EventArgs.Empty)
+                );
+            }
+        }
+
+        private void OnTaskGroupStopped ()
+        {
+            EventHandler<EventArgs> handler = Stopped;
+
+            if (handler != null) {
+                commandQueue.Register (
+                    new EventWrapper<EventArgs> (handler, this, EventArgs.Empty)
+                );
+            }
+        }
+
+        private void OnTaskEvent (T task, EventHandler<TaskEventArgs<T>> eventHandler)
+        {
+            if (eventHandler != null) {
+                commandQueue.Register (
+                    new EventWrapper<TaskEventArgs<T>> (eventHandler, this, new TaskEventArgs<T> (task))
+                );
+            }
+        }
+
+        private void PumpQueue ()
+        {
+            try {
+                PumpQueueImpl ();
+            } catch (Exception e) {
+                Console.WriteLine (e.Message);
+                Console.WriteLine (e.StackTrace);
+                throw;
+            }
+        }
+
+        private void PumpQueueImpl ()
+        {
+            T task;
+
+            while (true) {
+                gsm.Wait ();
+
+                lock (sync) {
+                    gsm.ResetWait ();
+
+                    if (IsDone) {
+                        if (SetExecuting (false)) {
+                            Reset ();
+                            OnStopped ();
+                            return;
+                        }
+                    } else if (cancelRequested) {
+                        continue;
+                    }
+
+                    task = list.Find (
+                        delegate (T t) {
+                            return (!t.IsBusy && !t.IsFinished && t.State == TaskState.Ready);
+                        }
+                    );
+
+                    if (task != null) {
+                        try {
+                            if (gsm.RegisterPendingTask (task)) {
+                                task.ExecuteAsync ();
+                            }
+                        } catch /*(Exception e)*/ {
+                            // Add an exception logging system that allows Hyena.Logging to hook in.
+                            // Implementations of 'ExecuteAsync' should never throw an exception.
+                            gsm.DropPendingTask (task);
+                        }
+                    }
+                }
+            }
+        }
+
+        private void SpawnExecutionThread ()
+        {
+            Thread t = new Thread (new ThreadStart (PumpQueue));
+            t.Priority = ThreadPriority.Normal;
+            t.IsBackground = true;
+            t.Start ();
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Async/TaskGroup/TaskGroup_Collection.cs b/src/Libraries/Migo2/Migo2.Async/TaskGroup/TaskGroup_Collection.cs
new file mode 100644
index 0000000..6f145a4
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Async/TaskGroup/TaskGroup_Collection.cs
@@ -0,0 +1,536 @@
+//
+// TaskGroup_Collection.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+using Migo2.Collections;
+
+namespace Migo2.Async
+{
+    public partial class TaskGroup<T> where T : Task
+    {
+        private List<T> list;
+
+        public EventHandler<ReorderedEventArgs> Reordered;
+        public EventHandler<TaskAddedEventArgs<T>> TaskAdded;
+
+        public int Count
+        {
+            get {
+                lock (sync) {
+                    return list.Count;
+                }
+            }
+        }
+
+        protected ICollection<T> Tasks
+        {
+            get { return list; }
+        }
+
+        private void InitTaskGroup_Collection ()
+        {
+            list = new List<T> ();
+        }
+
+        public T this [int index]
+        {
+            get {
+                lock (sync) {
+                    CheckDisposed ();
+                    CheckIndex (index);
+                    return list[index];
+                }
+            }
+
+            set {
+                lock (sync) {
+                    CheckDisposed ();
+                    CheckIndex (index);
+                    list[index] = value;
+                }
+            }
+        }
+
+        public void Add (T task)
+        {
+            CheckTask (task);
+
+            lock (sync) {
+                CheckDisposed ();
+
+                if (!CanAdd ()) {
+                    return;
+                }
+
+                int index = list.Count;
+
+                list.Add (task);
+                Associate (task);
+                gsm.IncrementRemainingTaskCount ();
+
+                OnTaskAdded (index, task);
+            }
+
+            Execute ();
+        }
+
+        public void Add (IEnumerable<T> tasks)
+        {
+            bool execute = false;
+            CheckTasks (tasks);
+
+            lock (sync) {
+                CheckDisposed ();
+
+                if (!CanAdd ()) {
+                    return;
+                }
+
+                int index = list.Count;
+                ICollection<Pair<int,T>> coll = CreatePairs (index, tasks);
+
+                if (coll.Count > 0) {
+                    execute = true;
+                    list.AddRange (tasks);
+                    Associate (tasks);
+                    gsm.RemainingTasks += coll.Count;
+
+                    OnTasksAdded (coll);
+                }
+            }
+
+            if (execute) {
+                Execute ();
+            }
+        }
+
+        public bool Contains (T task)
+        {
+            lock (sync) {
+                CheckDisposed ();
+
+                if (task == null) {
+                    return false;
+                } else {
+                    lock (sync) {
+                        return list.Contains (task);
+                    }
+                }
+            }
+        }
+
+        public void CopyTo (T[] array, int index)
+        {
+            lock (sync) {
+                CheckDisposed ();
+                CheckCopyArgs (array, index);
+                list.CopyTo (array, index);
+            }
+        }
+
+        public void CopyTo (Array array, int index)
+        {
+            lock (sync) {
+                CheckDisposed ();
+                CheckCopyArgs (array, index);
+                Array.Copy (list.ToArray (), 0, array, index, list.Count);
+            }
+        }
+
+        // I'd like to make the TaskGroup's SyncRoot private in the future, this would be a problem for GE.
+        public IEnumerator<T> GetEnumerator ()
+        {
+            foreach (T task in list) {
+                yield return task;
+            }
+        }
+
+        public int IndexOf (T task)
+        {
+            lock (sync) {
+                CheckDisposed ();
+
+                if (task == null) {
+                    return -1;
+                } else {
+                    lock (sync) {
+                        return list.IndexOf (task);
+                    }
+                }
+            }
+        }
+
+        public void Insert (int index, T task)
+        {
+            CheckTask (task);
+
+            lock (sync) {
+                CheckDisposed ();
+
+                if (!CanAdd ()) {
+                    return;
+                }
+
+                CheckDestIndex (index);
+
+                list.Insert (index, task);
+
+                OnTaskAdded (index, task);
+            }
+
+            Execute ();
+        }
+
+        public void InsertRange (int index, IEnumerable<T> tasks)
+        {
+            if (!CanAdd ()) {
+                return;
+            }
+
+            CheckTasks (tasks);
+
+            bool execute = false;
+
+            lock (sync) {
+                CheckDisposed ();
+                CheckDestIndex (index);
+
+                ICollection<Pair<int,T>> coll = CreatePairs (index, tasks);
+
+                if (coll.Count > 0) {
+                    list.InsertRange (index, tasks);
+
+                    Associate (tasks);
+                    gsm.RemainingTasks += coll.Count;
+
+                    OnTasksAdded (coll);
+                    execute = true;
+                }
+            }
+
+            if (execute) {
+                Execute ();
+            }
+        }
+
+        public void Move (int destIndex, int sourceIndex)
+        {
+            if (sourceIndex == destIndex) {
+                return;
+            }
+
+            lock (sync) {
+                CheckDisposed ();
+
+                if (sourceIndex >= list.Count || sourceIndex < 0) {
+                    throw new ArgumentOutOfRangeException ("sourceIndex");
+                } else if (destIndex > list.Count || destIndex < 0) {
+                    throw new ArgumentOutOfRangeException ("destIndex");
+                }
+
+                Dictionary<Task,int> oldOrder = SaveOrder ();
+
+                T tmpTask;
+
+                tmpTask = list[sourceIndex];
+                list.RemoveAt (sourceIndex);
+
+                list.Insert (Math.Min (list.Count, destIndex), tmpTask);
+
+                OnReordered (NewOrder (oldOrder));
+            }
+        }
+
+        public void Move (int destIndex, int[] sourceIndices)
+        {
+            if (sourceIndices == null) {
+                throw new ArgumentNullException ("sourceIndices");
+            }
+
+            lock (sync) {
+                CheckDisposed ();
+                int maxIndex = list.Count;
+
+                if (destIndex > maxIndex || destIndex < 0) {
+                    throw new ArgumentOutOfRangeException ("destIndex");
+                }
+
+                List<T> tmpList = new List<T> (sourceIndices.Length);
+
+                foreach (int i in sourceIndices) {
+                    if (i < 0 || i > maxIndex) {
+                        throw new ArgumentOutOfRangeException ("sourceIndices");
+                    }
+
+                    // A possible performance enhancement is to check for
+                    // contiguous regions in the source and remove those regions
+                    // at once.
+                    tmpList.Add (list[i]);
+                }
+
+                Dictionary<Task,int> oldOrder = SaveOrder ();
+
+                if (tmpList.Count > 0) {
+                    int offset = 0;
+                    Array.Sort (sourceIndices);
+
+                    foreach (int i in sourceIndices)
+                    {
+                        try {
+                            list.RemoveAt (i-offset);
+                        } catch { continue; }
+
+                        ++offset;
+                    }
+
+                    list.InsertRange (Math.Min (destIndex, list.Count), tmpList);
+                    OnReordered (NewOrder (oldOrder));
+                }
+            }
+        }
+
+        public void Move (int destIndex, T task)
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            }
+
+            int index;
+
+            lock (sync) {
+                CheckDisposed ();
+                index = list.IndexOf (task);
+
+                if (index != -1) {
+                    Move (destIndex, index);
+                }
+            }
+        }
+
+        public void Move (int destIndex, IEnumerable<T> tasks)
+        {
+            if (tasks == null) {
+                throw new ArgumentNullException ("tasks");
+            }
+
+            int index;
+            List<int> indices = new List<int> ();
+
+            lock (sync) {
+                CheckDisposed ();
+
+                foreach (T task in tasks) {
+                    index = list.IndexOf (task);
+                    if (index != -1) {
+                        indices.Add (index);
+                    }
+                }
+
+                if (indices.Count > 0) {
+                    Move (destIndex, indices.ToArray ());
+                }
+            }
+        }
+
+        public void Reverse ()
+        {
+            lock (sync) {
+                CheckDisposed ();
+                Dictionary<Task,int> oldOrder = SaveOrder ();
+                list.Reverse ();
+                OnReordered (NewOrder (oldOrder));
+            }
+        }
+
+        protected virtual IEnumerable<Pair<int,int>> DetectContinuity (int[] indices)
+        {
+            if (indices == null) {
+                throw new ArgumentNullException ("indices");
+            } else if (indices.Length == 0) {
+                return null;
+            }
+
+            int cnt;
+            int len = indices.Length;
+            List<Pair<int,int>> ret = new List<Pair<int,int>>();
+
+            int i = len-1;
+
+            while (i > 0) {
+                cnt = 1;
+                while (indices[i] == indices[i-1]+1)
+                {
+                    ++cnt;
+                    if (--i == 0) {
+                        break;
+                    }
+                }
+
+                ret.Add (new Pair<int,int>(indices[i--], cnt));
+            }
+
+            return ret;
+        }
+
+        protected virtual void OnReordered (int[] newOrder)
+        {
+            EventHandler<ReorderedEventArgs> handler = Reordered;
+
+            if (handler != null) {
+                ReorderedEventArgs e = new ReorderedEventArgs (newOrder);
+                commandQueue.Register (new EventWrapper<ReorderedEventArgs> (handler, this, e));
+            }
+        }
+
+        protected virtual void OnTaskAdded (int pos, T task)
+        {
+            EventHandler<TaskAddedEventArgs<T>> handler = TaskAdded;
+
+            if (handler != null) {
+                TaskAddedEventArgs<T> e = new TaskAddedEventArgs<T> (pos, task);
+                commandQueue.Register (new EventWrapper<TaskAddedEventArgs<T>> (handler, this, e));
+            }
+        }
+
+        protected virtual void OnTasksAdded (ICollection<Pair<int,T>> pairs)
+        {
+            EventHandler<TaskAddedEventArgs<T>> handler = TaskAdded;
+
+            if (handler != null) {
+                TaskAddedEventArgs<T> e = new TaskAddedEventArgs<T> (pairs);
+                commandQueue.Register (new EventWrapper<TaskAddedEventArgs<T>> (handler, this, e));
+            }
+        }
+
+        private void CheckCopyArgs (Array array, int index)
+        {
+            if (array == null) {
+                throw new ArgumentNullException ("array");
+            } else if (index < 0) {
+                throw new ArgumentOutOfRangeException (
+                    "Value of index must be greater than or equal to 0"
+                );
+            } else if (array.Rank > 1) {
+                throw new ArgumentException (
+                    "array must not be multidimensional"
+                );
+            } else if (index >= array.Length) {
+                throw new ArgumentException (
+                    "index exceeds array length"
+                );
+            } else if (list.Count > (array.Length-index)) {
+                throw new ArgumentException (
+                    "index and count exceed length of array"
+                );
+            }
+        }
+
+        private bool CanAdd ()
+        {
+            if (disposed) {
+                throw new ObjectDisposedException (GetType ().ToString ());
+            } else if (cancelRequested) {
+                throw new OperationCanceledException ();
+            }
+
+            return true;
+        }
+
+        private void CheckDestIndex (int index)
+        {
+            if (index < 0 || index > list.Count) {
+                throw new ArgumentOutOfRangeException ("index");
+            }
+        }
+
+        private void CheckIndex (int index)
+        {
+            if (index < 0 || index >= list.Count) {
+                throw new ArgumentOutOfRangeException ("index");
+            }
+        }
+
+        private void CheckTask (T task)
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            }
+        }
+
+        private void CheckTasks (IEnumerable<T> tasks)
+        {
+            if (tasks == null) {
+                throw new ArgumentNullException ("tasks");
+            }
+
+            foreach (Task t in tasks) {
+                if (t == null) {
+                    throw new ArgumentException (
+                        "No task in tasks may be null"
+                    );
+                }
+            }
+        }
+
+        private ICollection<Pair<int,T>> CreatePairs (int index, IEnumerable<T> tasks)
+        {
+            List<Pair<int,T>> pairs = new List<Pair<int,T>> ();
+
+            foreach (T task in tasks) {
+                pairs.Add (new Pair<int,T> (index++, task));
+            }
+
+            return pairs;
+        }
+
+        private int[] NewOrder (Dictionary<Task,int> oldOrder)
+        {
+            int i = -1;
+            int[] newOrder = new int[list.Count];
+
+            foreach (Task t in list) {
+                newOrder[++i] = oldOrder[t];
+            }
+
+            return newOrder;
+        }
+
+        private Dictionary<Task,int> SaveOrder ()
+        {
+            Dictionary<Task,int> ret = new Dictionary<Task,int> (list.Count);
+
+            int i = -1;
+
+            foreach (Task t in list) {
+                ret[t] = ++i;
+            }
+
+            return ret;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Libraries/Migo2/Migo2.Collections/OrderComparer.cs b/src/Libraries/Migo2/Migo2.Collections/OrderComparer.cs
new file mode 100644
index 0000000..4052ec8
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Collections/OrderComparer.cs
@@ -0,0 +1,53 @@
+//
+// OrderComparer.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System.Collections.Generic;
+
+namespace Migo2.Collections
+{
+    public class OrderComparer<T> : IComparer<T>
+    {
+        Dictionary<T, int> pos_dict;
+
+        public OrderComparer (Dictionary<T, int> posDict)
+        {
+            pos_dict = posDict;
+        }
+
+        public int Compare (T lhs, T rhs)
+        {
+            int lhs_pos = pos_dict[lhs], rhs_pos = pos_dict[rhs];
+
+            if (lhs_pos < rhs_pos) {
+                return -1;
+            } else if (lhs_pos > rhs_pos) {
+                return 1;
+            }
+
+            return 0;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Libraries/Migo2/Migo2.Collections/Pair.cs b/src/Libraries/Migo2/Migo2.Collections/Pair.cs
new file mode 100644
index 0000000..bdb66db
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Collections/Pair.cs
@@ -0,0 +1,43 @@
+//
+// Pair.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+namespace Migo2.Collections
+{
+    public class Pair<F,S>
+    {
+        private readonly F first;
+        private readonly S second;
+
+        public F First { get { return first; } }
+        public S Second { get { return second; } }
+
+        public Pair (F first, S second)
+        {
+            this.first = first;
+            this.second = second;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Libraries/Migo2/Migo2.DownloadService/DownloadStatusManager.cs b/src/Libraries/Migo2/Migo2.DownloadService/DownloadStatusManager.cs
new file mode 100644
index 0000000..b4e51f1
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.DownloadService/DownloadStatusManager.cs
@@ -0,0 +1,72 @@
+//
+// DownloadStatusManager.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Timers;
+using System.Collections.Generic;
+
+using Migo2.Async;
+
+namespace Migo2.DownloadService
+{
+    public class DownloadStatusManager : GroupStatusManager
+    {
+        private long bytesPerSecond;
+
+        public DownloadStatusManager () : this (0,0)
+        {
+        }
+
+        public DownloadStatusManager (int totalDownloads, int maxRunningDownloads)
+            : base (totalDownloads, maxRunningDownloads)
+        {
+        }
+
+        public override void Reset ()
+        {
+            bytesPerSecond = 0;
+            base.Reset ();
+        }
+
+        public virtual void SetTransferRate (long bytesPerSecond)
+        {
+            if (this.bytesPerSecond != bytesPerSecond) {
+                this.bytesPerSecond = bytesPerSecond;
+                OnStatusChanged ();
+            }
+        }
+
+        protected override void OnStatusChanged ()
+        {
+            base.OnStatusChanged (
+                new HttpDownloadGroupStatusChangedEventArgs (
+                    RemainingTasks, RunningTasks,
+                    CompletedTasks, bytesPerSecond
+                ) as GroupStatusChangedEventArgs
+            );
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.DownloadService/DownloadTaskStatusUpdatedEventArgs.cs b/src/Libraries/Migo2/Migo2.DownloadService/DownloadTaskStatusUpdatedEventArgs.cs
new file mode 100644
index 0000000..60d9d78
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.DownloadService/DownloadTaskStatusUpdatedEventArgs.cs
@@ -0,0 +1,53 @@
+//
+// DownloadTaskStatusUpdatedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Migo2.Net;
+
+namespace Migo2.DownloadService
+{
+    public class DownloadTaskStatusUpdatedEventArgs : EventArgs
+    {
+        private readonly DownloadStatus status;
+        private readonly HttpFileDownloadTask task;
+
+        public HttpFileDownloadTask Task
+        {
+            get { return task; }
+        }
+
+        public DownloadStatus Status {
+            get { return status; }
+        }
+
+        public DownloadTaskStatusUpdatedEventArgs (HttpFileDownloadTask task, DownloadStatus status)
+        {
+            this.task = task;
+            this.status = status;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Libraries/Migo2/Migo2.DownloadService/HttpDownloadGroup.cs b/src/Libraries/Migo2/Migo2.DownloadService/HttpDownloadGroup.cs
new file mode 100644
index 0000000..2d39e83
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.DownloadService/HttpDownloadGroup.cs
@@ -0,0 +1,233 @@
+//
+// HttpDownloadGroup.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.Timers;  // migrate to System.Threading.Timer
+using System.Threading;
+
+using System.ComponentModel;
+using System.Collections.Generic;
+
+using Hyena;
+
+using Migo2.Net;
+using Migo2.Async;
+
+namespace Migo2.DownloadService
+{
+    public class HttpDownloadGroup : TaskGroup<HttpFileDownloadTask>
+    {
+        private long transferRate;
+        private long transferRatePreviously;
+        private long bytesThisInterval = 0;
+
+        private DateTime lastTick;
+        private DownloadStatusManager dsm;
+        private System.Timers.Timer transferTimer;
+
+        private Dictionary<HttpFileDownloadTask,long> transferRateDict;
+
+        public EventHandler<DownloadTaskStatusUpdatedEventArgs> TaskStatusUpdated;
+
+        public HttpDownloadGroup (int maxDownloads) : base (maxDownloads, new DownloadStatusManager ())
+        {
+            dsm = StatusManager as DownloadStatusManager;
+            transferRateDict = new Dictionary<HttpFileDownloadTask,long> (dsm.MaxRunningTasks);
+
+            InitTransferTimer ();
+        }
+
+        // already locked at this point
+        protected override void Associate (HttpFileDownloadTask task, bool addToProgressGroup)
+        {
+            base.Associate (task, addToProgressGroup);
+            task.StatusUpdated += OnDownloadTaskStatusUpdatedHandler;
+        }
+
+        // already locked at this point
+        protected override void Disassociate (HttpFileDownloadTask task, bool removeFromGroup)
+        {
+            base.Disassociate (task, removeFromGroup);
+            task.StatusUpdated -= OnDownloadTaskStatusUpdatedHandler;
+        }
+
+        protected override void DisposeImpl ()
+        {
+            lock (SyncRoot) {
+                if (transferTimer != null) {
+                    transferTimer.Enabled = false;
+                    transferTimer.Elapsed -= OnTransmissionTimerElapsedHandler;
+                    transferTimer.Dispose ();
+                    transferTimer = null;
+                }
+
+                base.DisposeImpl ();
+            }
+        }
+
+        protected override void OnStarted ()
+        {
+            lock (SyncRoot) {
+                transferTimer.Enabled = true;
+                lastTick = DateTime.Now;
+                base.OnStarted ();
+            }
+        }
+
+        protected override void OnStopped ()
+        {
+            lock (SyncRoot) {
+                transferTimer.Enabled = false;
+                base.OnStopped ();
+            }
+        }
+
+        protected override void OnTaskStarted (HttpFileDownloadTask task)
+        {
+            lock (SyncRoot) {
+                transferRateDict.Add (task, task.BytesReceived);
+                base.OnTaskStarted (task);
+            }
+        }
+
+        protected override void OnTaskCompleted (HttpFileDownloadTask task, TaskCompletedEventArgs e)
+        {
+            lock (SyncRoot) {
+                if (transferRateDict.ContainsKey (task)) {
+                    long bytesLastCheck = transferRateDict[task];
+
+                    if (task.BytesReceived > bytesLastCheck) {
+                        bytesThisInterval += (task.BytesReceived - bytesLastCheck);
+                    }
+
+                    transferRateDict.Remove (task);
+                }
+
+                base.OnTaskCompleted (task, e);
+            }
+        }
+
+        protected virtual void SetTransferRate (long bytesPerSecond)
+        {
+            lock (SyncRoot) {
+                dsm.SetTransferRate (bytesPerSecond);
+            }
+        }
+
+        protected override void Reset ()
+        {
+            lastTick = DateTime.Now;
+
+            transferRate = -1;
+            transferRatePreviously = -1;
+
+            base.Reset ();
+        }
+
+        protected virtual void OnTransmissionTimerElapsedHandler (object source, ElapsedEventArgs e)
+        {
+            lock (SyncRoot) {
+                UpdateTransferRate ();
+            }
+        }
+
+        protected virtual void UpdateTransferRate ()
+        {
+            transferRate = CalculateTransferRate ();
+
+            if (transferRatePreviously == 0) {
+                transferRatePreviously = transferRate;
+            }
+
+            transferRate = ((transferRate + transferRatePreviously) / 2);
+
+            SetTransferRate (transferRate);
+            transferRatePreviously = transferRate;
+        }
+
+        private void InitTransferTimer ()
+        {
+            transferTimer = new System.Timers.Timer ();
+
+            transferTimer.Elapsed += OnTransmissionTimerElapsedHandler;
+            transferTimer.Interval = (1.5 * 1000); // 1.5 seconds
+            transferTimer.Enabled = false;
+        }
+
+        private long CalculateTransferRate ()
+        {
+            long bytesPerSecond;
+
+            TimeSpan duration = (DateTime.Now - lastTick);
+            double secondsElapsed = duration.TotalSeconds;
+
+            if ((int)secondsElapsed == 0) {
+                return 0;
+            }
+
+            long tmpCur;
+            long tmpPrev;
+
+            foreach (HttpFileDownloadTask dt in CurrentTasks) {
+                tmpCur = dt.BytesReceived;
+                tmpPrev = transferRateDict[dt];
+                transferRateDict[dt] = tmpCur;
+
+                bytesThisInterval += (tmpCur - tmpPrev);
+            }
+
+            bytesPerSecond = (long) ((bytesThisInterval / secondsElapsed));
+
+            lastTick = DateTime.Now;
+            bytesThisInterval = 0;
+
+            return bytesPerSecond;
+        }
+
+        protected virtual void OnDownloadTaskStatusUpdatedHandler (object sender, DownloadStatusUpdatedEventArgs e)
+        {
+            HttpFileDownloadTask task = sender as HttpFileDownloadTask;
+            OnDownloadTaskStatusUpdated (task, e.Status);
+        }
+
+        protected virtual void OnDownloadTaskStatusUpdated (HttpFileDownloadTask task, DownloadStatus status)
+        {
+            CommandQueue queue = EventQueue;
+            EventHandler<DownloadTaskStatusUpdatedEventArgs> handler = TaskStatusUpdated;
+
+            if (queue != null && handler != null) {
+                queue.Register (delegate { handler (this, new DownloadTaskStatusUpdatedEventArgs (task, status)); });
+            } else if (handler != null) {
+                ThreadPool.QueueUserWorkItem (
+                    delegate {
+                        handler (this, new DownloadTaskStatusUpdatedEventArgs (task, status));
+                    }
+                );
+            }
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.DownloadService/HttpDownloadGroupStatusChangedEventArgs.cs b/src/Libraries/Migo2/Migo2.DownloadService/HttpDownloadGroupStatusChangedEventArgs.cs
new file mode 100644
index 0000000..cb0e9a4
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.DownloadService/HttpDownloadGroupStatusChangedEventArgs.cs
@@ -0,0 +1,49 @@
+//
+// HttpDownloadGroupStatusChangedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+using Migo2.Async;
+
+namespace Migo2.DownloadService
+{
+    // Way too fucking long.
+    public class HttpDownloadGroupStatusChangedEventArgs : GroupStatusChangedEventArgs
+    {
+        private readonly long bytesPerSecond;
+
+        public long BytesPerSecond
+        {
+            get { return bytesPerSecond; }
+        }
+
+        public HttpDownloadGroupStatusChangedEventArgs (int totalTasks, int runningTasks, int completedTasks, long bytesPerSecond)
+            : base (totalTasks, runningTasks, completedTasks)
+        {
+            this.bytesPerSecond = bytesPerSecond;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.DownloadService/HttpDownloadManager.cs b/src/Libraries/Migo2/Migo2.DownloadService/HttpDownloadManager.cs
new file mode 100644
index 0000000..0ec4f89
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.DownloadService/HttpDownloadManager.cs
@@ -0,0 +1,164 @@
+//
+// HttpDownloadManager.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.Web;
+
+using System.Text;
+using System.Threading;
+using System.ComponentModel;
+using System.Collections.Generic;
+
+using System.Security.Cryptography;
+
+using Migo2.Async;
+using Migo2.Collections;
+
+namespace Migo2.DownloadService
+{
+    public class HttpDownloadManager : HttpDownloadGroup
+    {
+        private string tmp_dir;
+
+        public string TempDownloadDirectory {
+            get {
+                lock (SyncRoot) {
+                    return tmp_dir;
+                }
+            }
+
+            set {
+                if (String.IsNullOrEmpty (value)) {
+                    return;
+                }
+
+                lock (SyncRoot) {
+                    if (IsDisposing) {
+                        return;
+                    }
+
+                    if (!value.EndsWith (Convert.ToString (Path.DirectorySeparatorChar))) {
+                        value += Path.DirectorySeparatorChar;
+                    }
+
+                    if (!Directory.Exists (value)) {
+                        try {
+                            Directory.CreateDirectory (value);
+                        } catch (Exception e) {
+                            throw new ApplicationException (String.Format (
+                                "Unable to create temporary download directory:  {0}",
+                                e.Message
+                            ));
+                        }
+                    }
+
+                    tmp_dir = value;
+                }
+            }
+        }
+
+        public HttpDownloadManager (int maxDownloads, string tmpDownloadDir) : base (maxDownloads)
+        {
+            TempDownloadDirectory = tmpDownloadDir;
+        }
+
+        public HttpFileDownloadTask CreateDownloadTask (string url)
+        {
+            return CreateDownloadTask (url, null);
+        }
+
+        public HttpFileDownloadTask CreateDownloadTask (string url, object userState)
+        {
+            Uri uri;
+
+            if (String.IsNullOrEmpty (url)) {
+                throw new ArgumentException ("Cannot be null or empty", "url");
+            } else if (!Uri.TryCreate (url, UriKind.Absolute, out uri)) {
+                throw new UriFormatException ("url:  Is not a well formed Uri.");
+            }
+
+            string[] segments = uri.Segments;
+            string fileName = segments[segments.Length-1].Trim ('/');
+
+            MD5 hasher = MD5.Create ();
+            byte[] hash = hasher.ComputeHash (Encoding.UTF8.GetBytes (url));
+
+            string urlHash = BitConverter.ToString (hash)
+                                         .Replace ("-", String.Empty)
+                                         .ToLower ();
+
+            string localPath = String.Concat (
+                Path.Combine (TempDownloadDirectory, urlHash), Path.DirectorySeparatorChar,
+                Hyena.StringUtil.EscapeFilename (fileName)
+            );
+
+            return new HttpFileDownloadTask (
+                System.Web.HttpUtility.UrlDecode (fileName), url, localPath, userState
+            );
+        }
+
+
+        public override void Dispose ()
+        {
+            if (SetDisposing ()) {
+                StopAsync ();
+                Handle.WaitOne ();
+                base.Dispose ();
+            }
+        }
+
+        public void QueueDownload (HttpFileDownloadTask task)
+        {
+            if (task == null) {
+                throw new ArgumentNullException ("task");
+            }
+
+            lock (SyncRoot) {
+                if (IsDisposing) {
+                    return;
+                }
+
+                Add (task);
+            }
+        }
+
+        public void QueueDownload (IEnumerable<HttpFileDownloadTask> tasks)
+        {
+            if (tasks == null) {
+                throw new ArgumentNullException ("tasks");
+            }
+
+            lock (SyncRoot) {
+                if (IsDisposing) {
+                    return;
+                }
+
+                Add (tasks);
+            }
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.DownloadService/HttpFileDownloadErrors.cs b/src/Libraries/Migo2/Migo2.DownloadService/HttpFileDownloadErrors.cs
new file mode 100644
index 0000000..f8e1a3f
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.DownloadService/HttpFileDownloadErrors.cs
@@ -0,0 +1,38 @@
+//
+// HttpFileDownloadErrors.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+namespace Migo2.DownloadService
+{
+    // This is stupid.
+    public enum HttpFileDownloadErrors
+    {
+        None = 0,
+        HttpError,
+        SharingViolation,
+        UnauthorizedFileAccess,
+        Unknown
+    }
+}
\ No newline at end of file
diff --git a/src/Libraries/Migo2/Migo2.DownloadService/HttpFileDownloadTask.cs b/src/Libraries/Migo2/Migo2.DownloadService/HttpFileDownloadTask.cs
new file mode 100644
index 0000000..6c8283a
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.DownloadService/HttpFileDownloadTask.cs
@@ -0,0 +1,436 @@
+//
+// HttpFileDownloadTask.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.ComponentModel;
+using System.Security.Cryptography;
+
+using Migo2.Net;
+using Migo2.Async;
+
+namespace Migo2.DownloadService
+{
+    public class HttpFileDownloadTask : Task
+    {
+        private bool preexistingFile;
+
+        private int modified;
+        private int rangeError;
+
+        private string mimeType;
+        private Uri remoteUri;
+        private string localPath;
+
+        private long lastTotalBytes;
+        private long lastTotalBytesReceived;
+
+        private int timeout = (60 * 1000); // One minute
+        private string userAgent;
+
+        private HttpStatusCode httpStatus;
+        private HttpFileDownloadErrors error;
+
+        private AsyncWebClient wc;
+        private FileStream localStream;
+        private ICredentials credentials;
+
+        public EventHandler<DownloadStatusUpdatedEventArgs> StatusUpdated;
+
+        public long BytesReceived {
+            get {
+                lock (SyncRoot) {
+                    if (wc != null) {
+                        return wc.Status.BytesReceived;
+                    }
+                }
+
+                return 0;
+            }
+        }
+
+        public long TotalBytes {
+            get {
+                lock (SyncRoot) {
+                    if (wc != null) {
+                        if (wc.Status.TotalBytes > 0) {
+                            return wc.Status.TotalBytes;
+                        }
+                    }
+                }
+
+                return lastTotalBytes;
+            }
+        }
+
+        public long TotalBytesReceived {
+            get {
+                lock (SyncRoot) {
+                    if (wc != null) {
+                        if (wc.Status.TotalBytesReceived > 0) {
+                            return wc.Status.TotalBytesReceived;
+                        }
+                    }
+                }
+
+                return lastTotalBytesReceived;
+            }
+        }
+
+        public long TransferRate {
+            get {
+                lock (SyncRoot) {
+                    if (wc != null) {
+                        return wc.Status.TransferRate;
+                    }
+                }
+
+                return 0;
+            }
+        }
+
+        public ICredentials Credentials {
+            get { return credentials; }
+            set { credentials = value; }
+        }
+
+        public HttpFileDownloadErrors Error
+        {
+            get { return error; }
+        }
+
+        public HttpStatusCode HttpStatusCode
+        {
+            get { return httpStatus; }
+        }
+
+        public string LocalPath
+        {
+            get { return localPath; }
+        }
+
+        public string MimeType
+        {
+            get { return mimeType; }
+        }
+
+        public Uri RemoteUri
+        {
+            get { return remoteUri; }
+        }
+
+        public int Timeout {
+            get { return timeout; }
+            set { timeout = value; }
+        }
+
+        public string UserAgent {
+            get { return userAgent; }
+            set { userAgent = value; }
+        }
+
+        public HttpFileDownloadTask (string name, string remoteUri, string localPath, object userState)
+            : base (!String.IsNullOrEmpty (name) ? name : remoteUri, userState)
+        {
+            this.remoteUri = new Uri (remoteUri);
+            this.localPath = localPath;
+        }
+
+        public HttpFileDownloadTask (string remoteUri, string localPath) : this (null, remoteUri, localPath, null)
+        {
+        }
+
+        public override void CancelAsync ()
+        {
+            Cancel (CancellationType.Aborted);
+        }
+
+        public override void ExecuteAsync ()
+        {
+            lock (SyncRoot) {
+                if (SetBusy ()) {
+                    modified = 0;
+                    rangeError = 0;
+                    preexistingFile = false;
+
+                    OnStarted ();
+                    ExecuteImpl ();
+                }
+            }
+        }
+
+        public override void PauseAsync ()
+        {
+            Cancel (CancellationType.Paused);
+        }
+
+        public override void StopAsync ()
+        {
+            Cancel (CancellationType.Stopped);
+        }
+
+        private void Cancel (CancellationType type)
+        {
+            lock (SyncRoot) {
+                if (SetRequestedCancellationType (type)) {
+                    if (!IsBusy &&
+                        !IsFinished &&
+                        ((RequestedCancellationType == CancellationType.Paused) ? SetCompleted (true) : SetCompleted ())) {
+                        OnTaskCompleted (null, true);
+                    } else if (IsBusy) {
+                        wc.CancelAsync ();
+                    }
+                }
+            }
+        }
+
+        private void DestroyWebClient ()
+        {
+            if (wc != null) {
+                wc.DownloadFileCompleted -= OnDownloadFileCompletedHandler;
+                wc.DownloadProgressChanged -= OnDownloadProgressChangedHandler;
+                wc.ResponseReceived -= OnResponseReceivedHandler;
+                wc = null;
+            }
+        }
+
+        private void CloseLocalStream (bool removeFile)
+        {
+            try {
+                if (localStream != null) {
+                    localStream.Close ();
+                    localStream = null;
+                }
+            } catch {}
+
+            if (removeFile) {
+                RemoveFile ();
+            }
+        }
+
+        private void ExecuteImpl ()
+        {
+            Exception err = null;
+            bool fileOpenError = false;
+
+            try {
+                OpenLocalStream ();
+            } catch (Exception e) {
+                err = e;
+                fileOpenError = true;
+
+                if (e is UnauthorizedAccessException) {
+                    error = HttpFileDownloadErrors.UnauthorizedFileAccess;
+                } else if (e is IOException) {
+                    error = HttpFileDownloadErrors.SharingViolation; // Probably
+                } else {
+                    error = HttpFileDownloadErrors.Unknown;
+                }
+            }
+
+            if (err == null) {
+                try {
+                    InitWebClient ();
+                    wc.DownloadFileAsync (remoteUri, localStream);
+                } catch (Exception e) {
+                    err = e;
+                }
+            }
+
+            if (err != null) {
+                if (!fileOpenError) {
+                    CloseLocalStream (true);
+                }
+
+                if (SetCompleted ()) {
+                    DestroyWebClient ();
+                    OnTaskCompleted (err, false);
+                }
+            }
+        }
+
+        private void InitWebClient ()
+        {
+            wc = new AsyncWebClient ();
+
+            if (localStream.Length > 0) {
+                wc.Range = Convert.ToInt32 (localStream.Length);
+            }
+
+            wc.Timeout = timeout;
+
+            if (credentials != null) {
+                wc.Credentials = credentials;
+            }
+
+            if (!String.IsNullOrEmpty (userAgent)) {
+                wc.UserAgent = userAgent;
+            }
+
+            wc.DownloadFileCompleted += OnDownloadFileCompletedHandler;
+            wc.DownloadProgressChanged += OnDownloadProgressChangedHandler;
+            wc.ResponseReceived += OnResponseReceivedHandler;
+            wc.StatusUpdated += OnWebClientStatusUpdatedHandler;
+        }
+
+        private void OpenLocalStream ()
+        {
+            if (File.Exists (localPath)) {
+                localStream = File.Open (localPath, FileMode.Append, FileAccess.Write, FileShare.None);
+
+                preexistingFile = true;
+            } else {
+                preexistingFile = false;
+
+                if (!Directory.Exists (Path.GetDirectoryName (localPath))) {
+                    Directory.CreateDirectory (Path.GetDirectoryName (localPath));
+                }
+
+                localStream = File.Open (localPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
+            }
+        }
+
+        private void RemoveFile ()
+        {
+            if (File.Exists (localPath)) {
+                try {
+                    preexistingFile = false;
+                    File.Delete (localPath);
+                    Directory.Delete (Path.GetDirectoryName (localPath));
+                } catch {}
+            }
+        }
+
+        private void OnDownloadFileCompletedHandler (object sender, AsyncCompletedEventArgs e)
+        {
+            bool retry = false;
+
+            lock (SyncRoot) {
+                try {
+                    if (e.Error != null) {
+                        WebException we = e.Error as WebException;
+
+                        if (we != null) {
+                            if(we.Status == WebExceptionStatus.ProtocolError) {
+                                HttpWebResponse resp = we.Response as HttpWebResponse;
+
+                                if (resp != null) {
+                                    httpStatus = resp.StatusCode;
+                                    // This is going to get triggered if the file on disk is complete.
+                                    // Maybe request range-1 and see if a content length of 0 is returned.
+                                    if (resp.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) {
+                                        if (rangeError++ == 0) {
+                                            retry = true;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+
+                        if (!retry) {
+                            error = HttpFileDownloadErrors.HttpError;
+                        }
+                    } else if (modified++ == 1) {
+                        retry = true;
+                    }
+                } catch (Exception ex) {
+                    Console.WriteLine (ex.Message);
+                    Console.WriteLine (ex.StackTrace);
+                } finally {
+                    if (retry) {
+                        CloseLocalStream (true);
+                        DestroyWebClient ();
+                        ExecuteImpl ();
+                    } else if (
+                        ((RequestedCancellationType == CancellationType.Paused) ? SetCompleted (true) : SetCompleted ())
+                    ) {
+                        if (e.Cancelled && RequestedCancellationType == CancellationType.Aborted) {
+                            CloseLocalStream (true);
+                        } else {
+                            CloseLocalStream (false);
+                        }
+
+                        DestroyWebClient ();
+                        OnTaskCompleted (e.Error, e.Cancelled);
+                    }
+                }
+            }
+        }
+
+        private void OnDownloadProgressChangedHandler (object sender, Migo2.Net.DownloadProgressChangedEventArgs e)
+        {
+            lock (SyncRoot) {
+                if (e.ProgressPercentage != 0) {
+                    SetProgress (e.ProgressPercentage);
+                }
+            }
+        }
+
+        private void OnResponseReceivedHandler (object sender, EventArgs e)
+        {
+            lock (SyncRoot) {
+                if (wc != null && wc.ResponseHeaders != null) {
+                    mimeType = wc.ResponseHeaders.Get ("Content-Type");
+
+                    httpStatus = wc.Response.StatusCode;
+
+                    if (preexistingFile &&
+                        wc.Response.LastModified.ToUniversalTime () > File.GetLastWriteTimeUtc (localPath)) {
+                        ++modified;
+                        wc.CancelAsync ();
+                    }
+                }
+            }
+        }
+
+        protected virtual void OnWebClientStatusUpdatedHandler (object sender, DownloadStatusUpdatedEventArgs e)
+        {
+            lock (SyncRoot) {
+                lastTotalBytes = e.Status.TotalBytes;
+                lastTotalBytesReceived = e.Status.TotalBytesReceived;
+            }
+
+            OnDownloadStatusUpdated (e.Status);
+        }
+
+        protected virtual void OnDownloadStatusUpdated (DownloadStatus status)
+        {
+            CommandQueue queue = EventQueue;
+            EventHandler<DownloadStatusUpdatedEventArgs> handler = StatusUpdated;
+
+            if (queue != null && handler != null) {
+                queue.Register (delegate { handler (this, new DownloadStatusUpdatedEventArgs (status, UserState)); });
+            } else if (handler != null) {
+                ThreadPool.QueueUserWorkItem (
+                    delegate { handler (this, new DownloadStatusUpdatedEventArgs (status, UserState)); }
+                );
+            }
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/AsyncWebClient.cs b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/AsyncWebClient.cs
new file mode 100644
index 0000000..59d9e7a
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/AsyncWebClient.cs
@@ -0,0 +1,834 @@
+//
+// AsyncWebClient.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.ComponentModel;
+
+namespace Migo2.Net
+{
+    enum DownloadType
+    {
+        None = 0,
+        Data = 1,
+        File = 2,
+        String = 3
+    };
+
+    public sealed class AsyncWebClient
+    {
+        private int range = 0;
+        private int timeout = (120 * 1000); // 2 minutes
+        private DateTime ifModifiedSince = DateTime.MinValue;
+
+        private string fileName;
+
+        private Exception error;
+
+        private Uri uri;
+        private object userState;
+        private DownloadType type;
+
+        private IWebProxy proxy;
+        private string user_agent;
+        private Encoding encoding;
+        private ICredentials credentials;
+
+        private HttpWebRequest request;
+        private HttpWebResponse response;
+
+        private WebHeaderCollection headers;
+        private WebHeaderCollection responseHeaders;
+
+        private byte[] result;
+        private Stream localFile;
+        private MemoryStream memoryStream;
+
+        private TransferStatusManager tsm;
+
+        private AutoResetEvent readTimeoutHandle;
+        private RegisteredWaitHandle registeredTimeoutHandle;
+
+        private bool busy;
+        private bool completed;
+        private bool cancelled;
+
+        private readonly object cancelBusySync = new object ();
+
+        public event EventHandler<EventArgs> ResponseReceived;
+        public event EventHandler<DownloadProgressChangedEventArgs> DownloadProgressChanged;
+        public event EventHandler<DownloadStatusUpdatedEventArgs> StatusUpdated;
+
+        public event EventHandler<AsyncCompletedEventArgs> DownloadFileCompleted;
+        public event EventHandler<DownloadDataCompletedEventArgs> DownloadDataCompleted;
+        public event EventHandler<DownloadStringCompletedEventArgs> DownloadStringCompleted;
+
+        public ICredentials Credentials
+        {
+            get { return credentials; }
+            set { credentials = value; }
+        }
+
+        public Encoding Encoding
+        {
+            get { return encoding; }
+
+            set {
+                if (value == null) {
+                    throw new ArgumentNullException ("encoding");
+                } else {
+                    encoding = Encoding.Default;
+                }
+            }
+        }
+
+        public WebHeaderCollection Headers
+        {
+            get { return headers; }
+            set {
+                if (value == null) {
+                    headers = new WebHeaderCollection ();
+                } else {
+                    headers = value;
+                }
+            }
+        }
+
+        public DateTime IfModifiedSince
+        {
+            get { return ifModifiedSince; }
+            set { ifModifiedSince = value; }
+        }
+
+        public bool IsBusy
+        {
+            get { lock (cancelBusySync) { return busy; } }
+        }
+
+        public IWebProxy Proxy
+        {
+            get { return proxy; }
+            set { proxy = value; }
+        }
+
+        public int Range
+        {
+            get { return range; }
+            set {
+                if (range > -1) {
+                    range = value;
+                } else {
+                    throw new ArgumentOutOfRangeException ("Range");
+                }
+            }
+        }
+
+        public HttpWebResponse Response {
+            get { return response; }
+        }
+
+        public WebHeaderCollection ResponseHeaders {
+            get { return responseHeaders; }
+        }
+
+        public DownloadStatus Status
+        {
+            get {
+                if (type == DownloadType.String) {
+                    throw new InvalidOperationException (
+                        "Status cannot be reported for string downloads"
+                    );
+                }
+
+                lock (tsm.SyncRoot) {
+                    return new DownloadStatus (
+                        tsm.Progress, tsm.BytesReceived, tsm.TotalBytes, tsm.TotalBytesReceived, tsm.TransferRate
+                    );
+                }
+            }
+        }
+
+        public int Timeout {
+            get { return timeout; }
+            set {
+                if (value < -1) {
+                    throw new ArgumentOutOfRangeException (
+                        "Value must be greater than or equal to -1"
+                    );
+                }
+
+                timeout = value;
+            }
+        }
+
+        private static string default_user_agent;
+        public static string DefaultUserAgent {
+            get { return default_user_agent; }
+            set { default_user_agent = value; }
+        }
+
+        public string UserAgent {
+            get { return user_agent ?? DefaultUserAgent; }
+            set { user_agent = value; }
+        }
+
+        private bool Cancelled {
+            get { lock (cancelBusySync) { return cancelled; } }
+        }
+
+        public AsyncWebClient ()
+        {
+            encoding = Encoding.Default;
+            tsm = new TransferStatusManager ();
+        }
+
+        public void DownloadDataAsync (Uri address)
+        {
+            DownloadDataAsync (address, null);
+        }
+
+        public void DownloadDataAsync (Uri address, object userState)
+        {
+            if (address == null) {
+                throw new ArgumentNullException ("address");
+            }
+
+            SetBusy ();
+            DownloadAsync (address, DownloadType.Data, userState);
+        }
+
+        public void DownloadFileAsync (Uri address, string fileName)
+        {
+            DownloadFileAsync (address, fileName, null, null);
+        }
+
+        public void DownloadFileAsync (Uri address, Stream file)
+        {
+            DownloadFileAsync (address, null, file, null);
+        }
+
+        public void DownloadFileAsync (Uri address, string file, object userState)
+        {
+            DownloadFileAsync (address, file, null, userState);
+        }
+
+        public void DownloadFileAsync (Uri address, Stream file, object userState)
+        {
+            DownloadFileAsync (address, null, file, userState);
+        }
+
+        private void DownloadFileAsync (Uri address, string filePath, Stream fileStream, object userState)
+        {
+            if (String.IsNullOrEmpty (filePath) && fileStream == null || fileStream == Stream.Null) {
+                throw new ArgumentNullException ("file");
+            } else if (address == null) {
+                throw new ArgumentNullException ("address");
+            } else if (fileStream != null) {
+                if (!fileStream.CanWrite) {
+                    throw new ArgumentException ("Cannot write to stream");
+                } else {
+                    localFile = fileStream;
+                }
+            } else {
+                this.fileName = filePath;
+            }
+
+
+            SetBusy ();
+            DownloadAsync (address, DownloadType.File, userState);
+        }
+
+        public void DownloadStringAsync (Uri address)
+        {
+            DownloadStringAsync (address, null);
+        }
+
+        public void DownloadStringAsync (Uri address, object userState)
+        {
+            if (address == null) {
+                throw new ArgumentNullException ("address");
+            }
+
+            SetBusy ();
+            DownloadAsync (address, DownloadType.String, userState);
+        }
+
+        public void CancelAsync ()
+        {
+            CancelAsync (true);
+        }
+
+        public void CancelAsync (bool deleteFile)
+        {
+            if (SetCancelled ()) {
+                AbortDownload ();
+            }
+        }
+
+        private void AbortDownload ()
+        {
+            AbortDownload (null);
+        }
+
+        private void AbortDownload (Exception e)
+        {
+            error = e;
+
+            try {
+                HttpWebRequest req = request;
+
+                if (req != null) {
+                    req.Abort();
+                }
+            } catch {}
+        }
+
+        private void Completed ()
+        {
+            Completed (null);
+        }
+
+        private void Completed (Exception e)
+        {
+            Exception err = (SetCompleted ()) ? e : error;
+
+            object statePtr = userState;
+            byte[] resultPtr = result;
+            bool cancelledCpy = Cancelled;
+
+            CleanUp ();
+
+            DownloadCompleted (resultPtr, err, cancelledCpy, statePtr);
+
+            Reset ();
+        }
+
+        private void CleanUp ()
+        {
+            if (localFile != null) {
+                localFile.Close ();
+                localFile = null;
+            }
+
+            if (memoryStream != null) {
+                memoryStream.Close ();
+                memoryStream = null;
+            }
+
+            if (response != null) {
+                response.Close ();
+                response = null;
+            }
+
+            result = null;
+            request = null;
+
+            CleanUpHandles ();
+        }
+
+        private void CleanUpHandles ()
+        {
+            if (registeredTimeoutHandle != null) {
+                registeredTimeoutHandle.Unregister (readTimeoutHandle);
+                readTimeoutHandle = null;
+            }
+
+            if (readTimeoutHandle != null) {
+                readTimeoutHandle.Close ();
+                readTimeoutHandle = null;
+            }
+        }
+
+        private bool SetBusy ()
+        {
+            bool ret = false;
+
+            lock (cancelBusySync) {
+                if (busy) {
+                    throw new InvalidOperationException (
+                        "Concurrent transfer operations are not supported."
+                    );
+                } else {
+                    ret = busy = true;
+                }
+            }
+
+            return ret;
+        }
+
+        private bool SetCancelled ()
+        {
+            bool ret = false;
+
+            lock (cancelBusySync) {
+                if (busy && !completed && !cancelled) {
+                    ret = cancelled = true;
+                }
+            }
+
+            return ret;
+        }
+
+        private bool SetCompleted ()
+        {
+            bool ret = false;
+
+            lock (cancelBusySync) {
+                if (busy && !completed && !cancelled) {
+                    ret = completed = true;
+                }
+            }
+
+            return ret;
+        }
+
+        private void DownloadAsync (Uri uri, DownloadType type, object state)
+        {
+            this.uri = uri;
+            this.type = type;
+            this.userState = state;
+
+            ImplDownloadAsync ();
+        }
+
+        private void ImplDownloadAsync ()
+        {
+            try {
+                tsm.Reset ();
+                request = PrepRequest (uri);
+                IAsyncResult ar = request.BeginGetResponse (OnResponseCallback, null);
+
+                ThreadPool.RegisterWaitForSingleObject (
+                    ar.AsyncWaitHandle,
+                    new WaitOrTimerCallback(OnTimeout),
+                    request, timeout, true
+                );
+            } catch (Exception e) {
+                Completed (e);
+            }
+        }
+
+        private HttpWebRequest PrepRequest (Uri address)
+        {
+            responseHeaders = null;
+            HttpWebRequest req = HttpWebRequest.Create (address) as HttpWebRequest;
+
+            req.AllowAutoRedirect = true;
+            req.Credentials = credentials;
+
+            if (proxy != null) {
+                req.Proxy = proxy;
+            }
+
+            if (headers != null && headers.Count != 0) {
+
+                int rangeHdr = -1;
+                string expect = headers ["Expect"];
+                string contentType = headers ["Content-Type"];
+                string accept = headers ["Accept"];
+                string connection = headers ["Connection"];
+                string userAgent = headers ["User-Agent"];
+                string referer = headers ["Referer"];
+                string rangeStr = headers ["Range"];
+                string ifModifiedSince = headers ["If-Modified-Since"];
+
+                if (!String.IsNullOrEmpty (rangeStr)) {
+                    Int32.TryParse (rangeStr, out rangeHdr);
+                }
+
+                headers.Remove ("Expect");
+                headers.Remove ("Content-Type");
+                headers.Remove ("Accept");
+                headers.Remove ("Connection");
+                headers.Remove ("Referer");
+                headers.Remove ("User-Agent");
+                headers.Remove ("Range");
+                headers.Remove ("If-Modified-Since");
+
+                req.Headers = headers;
+
+                if (!String.IsNullOrEmpty (expect)) {
+                    req.Expect = expect;
+                }
+
+                if (!String.IsNullOrEmpty (accept)) {
+                    req.Accept = accept;
+                }
+
+                if (!String.IsNullOrEmpty (contentType)) {
+                    req.ContentType = contentType;
+                }
+
+                if (!String.IsNullOrEmpty (connection)) {
+                    req.Connection = connection;
+                }
+
+                if (!String.IsNullOrEmpty (userAgent)) {
+                    req.UserAgent = userAgent;
+                }
+
+                if (!String.IsNullOrEmpty (referer)) {
+                    req.Referer = referer;
+                }
+
+                if (rangeHdr > 0) {
+                    req.AddRange (range);
+                }
+
+                if (!String.IsNullOrEmpty (ifModifiedSince)) {
+                    DateTime modDate;
+
+                    if (DateTime.TryParse (ifModifiedSince, out modDate)) {
+                        req.IfModifiedSince = modDate;
+                    }
+                }
+            } else {
+                if (!String.IsNullOrEmpty (UserAgent)) {
+                    req.UserAgent = UserAgent;
+                }
+
+                if (this.range > 0) {
+                    req.AddRange (this.range);
+                }
+
+                if (this.ifModifiedSince > DateTime.MinValue) {
+                    req.IfModifiedSince = this.ifModifiedSince;
+                }
+            }
+
+            responseHeaders = null;
+
+            return req;
+        }
+
+        private void OnResponseCallback (IAsyncResult ar)
+        {
+            Exception err = null;
+            bool redirect_workaround = false;
+
+            try {
+                response = request.EndGetResponse (ar) as HttpWebResponse;
+
+                responseHeaders = response.Headers;
+                OnResponseReceived ();
+
+                if (type != DownloadType.String) {
+                    tsm.StatusUpdated += OnDownloadStatusUpdatedHandler;
+                    tsm.ProgressChanged += OnDownloadProgressChangedHandler;
+                    tsm.EnableStatusReporting = true;
+                }
+
+                Download (response.GetResponseStream ());
+            } catch (ObjectDisposedException) {
+            } catch (WebException we) {
+                if (we.Status != WebExceptionStatus.RequestCanceled) {
+                    err = we;
+                    HttpWebResponse response = we.Response as HttpWebResponse;
+
+                    if (response != null &&
+                        response.StatusCode == HttpStatusCode.BadRequest &&
+                        response.ResponseUri != request.RequestUri) {
+                        redirect_workaround = true;
+                        uri = response.ResponseUri;
+                        ImplDownloadAsync ();
+                    }
+                }
+            } catch (Exception e) {
+                err = e;
+            } finally {
+                if (type != DownloadType.String) {
+                    tsm.EnableStatusReporting = false;
+                    tsm.ProgressChanged -= OnDownloadProgressChangedHandler;
+                    tsm.StatusUpdated -= OnDownloadStatusUpdatedHandler;
+                }
+
+                if (!redirect_workaround) {
+                    Completed (err);
+                }
+            }
+        }
+
+        // All of this download code could be abstracted
+        // and put into a helper class.
+        private void Download (Stream st)
+        {
+            long cLength = (response.ContentLength + range);
+
+            if (cLength == 0) {
+                return;
+            }
+
+            int nread = -1;
+            int offset = 0;
+
+            int length = (cLength == -1 || cLength > 8192) ? 8192 : (int) cLength;
+
+            Stream dest = null;
+            readTimeoutHandle = new AutoResetEvent (false);
+
+            byte[] buffer = null;
+
+            bool dataDownload = false;
+            bool writeToStream = false;
+
+            if (type != DownloadType.String) {
+                tsm.TotalBytes = cLength;
+                tsm.BytesReceivedPreviously = range;
+            }
+
+            switch (type) {
+            case DownloadType.String:
+            case DownloadType.Data:
+                dataDownload = true;
+
+                if (cLength != -1) {
+                    length = (int) cLength;
+                    buffer = new byte[cLength];
+                } else {
+                    writeToStream = true;
+                    buffer = new byte[length];
+                    dest = OpenMemoryStream ();
+                }
+                break;
+            case DownloadType.File:
+                writeToStream = true;
+                buffer = new byte [length];
+
+                if (localFile == null) {
+                    dest = OpenLocalFile (fileName);
+                } else {
+                    dest = localFile;
+                }
+
+                break;
+            }
+
+            registeredTimeoutHandle = ThreadPool.RegisterWaitForSingleObject (
+                readTimeoutHandle, new WaitOrTimerCallback (OnTimeout), null, timeout, false
+            );
+
+            IAsyncResult ar;
+            while (nread != 0) {
+                // <hack>
+                // Yeah, Yeah, Yeah, I'll change this later,
+                // it's here to get around abort issues.
+
+                ar = st.BeginRead (buffer, offset, length, null, null);
+                nread = st.EndRead (ar);
+
+                // need an auxiliary downloader class to replace this.
+                // </hack>
+
+                readTimeoutHandle.Set ();
+
+                if (writeToStream) {
+                    dest.Write (buffer, 0, nread);
+                } else {
+                    offset += nread;
+                    length -= nread;
+                }
+
+                if (type != DownloadType.String) {
+                    tsm.AddBytes (nread);
+                }
+            }
+
+            CleanUpHandles ();
+
+            if (type != DownloadType.String) {
+                if (tsm.TotalBytes == -1) {
+                    tsm.TotalBytes = tsm.BytesReceived;
+                }
+            }
+
+            if (dataDownload) {
+                if (writeToStream) {
+                    result = memoryStream.ToArray ();
+                } else {
+                    result = buffer;
+                }
+            }
+        }
+
+        private Stream OpenLocalFile (string filePath)
+        {
+            return File.Open (filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
+        }
+
+        private MemoryStream OpenMemoryStream ()
+        {
+            return memoryStream = new MemoryStream ();
+        }
+
+        private void Reset ()
+        {
+            lock (cancelBusySync) {
+                busy = false;
+                cancelled = false;
+                completed = false;
+                error = null;
+                fileName = String.Empty;
+                ifModifiedSince = DateTime.MinValue;
+                range = 0;
+                type = DownloadType.None;
+                uri = null;
+                userState = null;
+            }
+        }
+
+        private void DownloadCompleted (byte[] resultPtr, Exception errPtr, bool cancelledCpy, object userStatePtr)
+        {
+            switch (type) {
+            case DownloadType.Data:
+                OnDownloadDataCompleted (resultPtr, errPtr, cancelledCpy, userStatePtr);
+                break;
+            case DownloadType.File:
+                OnDownloadFileCompleted (errPtr, cancelledCpy, userStatePtr);
+                break;
+            case DownloadType.String:
+                string s;
+                try {
+                    s = Encoding.GetString (resultPtr);
+                } catch {
+                    s = String.Empty;
+                }
+
+                OnDownloadStringCompleted (s, errPtr, cancelledCpy, userStatePtr);
+                break;
+            }
+        }
+
+        private void OnTimeout (object state, bool timedOut)
+        {
+            if (timedOut) {
+                if (SetCompleted ()) {
+                    try {
+                        AbortDownload (
+                            new WebException ("The operation timed out", null, WebExceptionStatus.Timeout, response)
+                        );
+                    } finally {
+                        Completed ();
+                    }
+                }
+            }
+        }
+
+        private void OnResponseReceived ()
+        {
+            EventHandler<EventArgs> handler = ResponseReceived;
+
+            if (handler != null) {
+                handler (this, new EventArgs ());
+            }
+        }
+
+        private void OnDownloadProgressChanged (long bytesReceived,
+                                                long BytesToReceive,
+                                                int progressPercentage,
+                                                object userState)
+        {
+            OnDownloadProgressChanged (
+                new DownloadProgressChangedEventArgs (
+                    progressPercentage, userState,
+                    bytesReceived, BytesToReceive
+                )
+            );
+        }
+
+        private void OnDownloadProgressChanged (DownloadProgressChangedEventArgs args)
+        {
+            EventHandler <DownloadProgressChangedEventArgs> handler = DownloadProgressChanged;
+
+            if (handler != null) {
+                handler (this, args);
+            }
+        }
+
+        private void OnDownloadProgressChangedHandler (object sender, DownloadProgressChangedEventArgs e)
+        {
+            OnDownloadProgressChanged (e.BytesReceived, e.TotalBytesToReceive, e.ProgressPercentage, userState);
+        }
+
+        private void OnDownloadStatusUpdatedHandler (object sender, DownloadStatusUpdatedEventArgs e)
+        {
+            OnDownloadStatusUpdated (e.Status);
+        }
+
+        private void OnDownloadDataCompleted (byte[] bytes, Exception error, bool cancelled, object userState)
+        {
+            OnDownloadDataCompleted (new DownloadDataCompletedEventArgs (bytes, error, cancelled, userState));
+        }
+
+        private void OnDownloadDataCompleted (DownloadDataCompletedEventArgs args)
+        {
+            EventHandler <DownloadDataCompletedEventArgs> handler = DownloadDataCompleted;
+
+            if (handler != null) {
+                handler (this, args);
+            }
+        }
+
+        private void OnDownloadFileCompleted (Exception error, bool cancelled, object userState)
+        {
+            OnDownloadFileCompleted (new AsyncCompletedEventArgs (error, cancelled, userState));
+        }
+
+        private void OnDownloadFileCompleted (AsyncCompletedEventArgs args)
+        {
+            EventHandler <AsyncCompletedEventArgs> handler = DownloadFileCompleted;
+
+            if (handler != null) {
+                handler (this, args);
+            }
+        }
+
+        private void OnDownloadStatusUpdated (DownloadStatus status)
+        {
+            EventHandler <DownloadStatusUpdatedEventArgs> handler = StatusUpdated;
+
+            if (handler != null) {
+                handler (this, new DownloadStatusUpdatedEventArgs (status, userState));
+            }
+        }
+
+        private void OnDownloadStringCompleted (string resultStr, Exception error, bool cancelled, object userState)
+        {
+            OnDownloadStringCompleted (new DownloadStringCompletedEventArgs (resultStr, error, cancelled, userState));
+        }
+
+        private void OnDownloadStringCompleted (DownloadStringCompletedEventArgs args)
+        {
+            EventHandler <DownloadStringCompletedEventArgs> handler = DownloadStringCompleted;
+
+            if (handler != null) {
+                handler (this, args);
+            }
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadDataCompletedEventArgs.cs b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadDataCompletedEventArgs.cs
new file mode 100644
index 0000000..c8e7e68
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadDataCompletedEventArgs.cs
@@ -0,0 +1,49 @@
+//
+// DownloadDataCompletedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.ComponentModel;
+
+namespace Migo2.Net
+{
+    public class DownloadDataCompletedEventArgs : AsyncCompletedEventArgs
+    {
+        private readonly byte[] result;
+
+        public byte[] Result {
+            get {
+                RaiseExceptionIfNecessary ();
+                return result;
+            }
+        }
+
+        internal DownloadDataCompletedEventArgs (byte[] result, Exception error, bool cancelled, object userState)
+            : base (error, cancelled, userState)
+        {
+            this.result = result;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadProgressChangedEventArgs.cs b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadProgressChangedEventArgs.cs
new file mode 100644
index 0000000..8f83b03
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadProgressChangedEventArgs.cs
@@ -0,0 +1,61 @@
+//
+// DownloadProgressChangedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.ComponentModel;
+
+namespace Migo2.Net
+{
+    public class DownloadProgressChangedEventArgs : ProgressChangedEventArgs
+    {
+        private readonly long totalBytes;
+        private readonly long bytesReceived;
+
+        public long BytesReceived
+        {
+            get {
+                return bytesReceived;
+            }
+        }
+
+        public long TotalBytesToReceive
+        {
+            get {
+                return totalBytes;
+            }
+        }
+
+        public DownloadProgressChangedEventArgs (int progressPercentage,
+                                                 object userState,
+                                                 long bytesReceived,
+                                                 long totalBytes)
+            : base (progressPercentage, userState)
+        {
+            this.totalBytes = totalBytes;
+            this.bytesReceived = bytesReceived;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadStatus.cs b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadStatus.cs
new file mode 100644
index 0000000..3f93c78
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadStatus.cs
@@ -0,0 +1,75 @@
+//
+// DownloadStatus.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+namespace Migo2.Net
+{
+    public struct DownloadStatus
+    {
+        private readonly int progress;
+        private readonly long bytesReceived;
+        private readonly long totalBytes;
+        private readonly long totalBytesReceived;
+        private readonly long transferRate;
+
+        public long BytesReceived
+        {
+            get { return bytesReceived; }
+        }
+
+        public int Progress
+        {
+            get { return progress; }
+        }
+
+        public long TotalBytes
+        {
+            get { return totalBytes; }
+        }
+
+        public long TotalBytesReceived
+        {
+            get { return totalBytesReceived; }
+        }
+
+        public long TransferRate {
+            get { return transferRate; }
+        }
+
+        public DownloadStatus (int progress,
+                               long bytesReceived,
+                               long totalBytes,
+                               long totalBytesReceived,
+                               long transferRate)
+        {
+            this.progress = progress;
+            this.bytesReceived = bytesReceived;
+            this.totalBytes = totalBytes;
+            this.totalBytesReceived = totalBytesReceived;
+            this.transferRate = transferRate;
+        }
+    }
+}
+
diff --git a/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadStatusUpdatedEventArgs.cs b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadStatusUpdatedEventArgs.cs
new file mode 100644
index 0000000..d0a801a
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadStatusUpdatedEventArgs.cs
@@ -0,0 +1,51 @@
+//
+// DownloadStatusUpdatedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Net
+{
+    public class DownloadStatusUpdatedEventArgs : EventArgs
+    {
+        private readonly DownloadStatus status;
+        private readonly object userState;
+
+        public DownloadStatus Status {
+            get { return status; }
+        }
+
+        public object UserState
+        {
+            get { return userState; }
+        }
+
+        public DownloadStatusUpdatedEventArgs (DownloadStatus status, object userState)
+        {
+            this.status = status;
+            this.userState = userState;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadStringCompletedEventArgs.cs b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadStringCompletedEventArgs.cs
new file mode 100644
index 0000000..8267645
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/DownloadStringCompletedEventArgs.cs
@@ -0,0 +1,53 @@
+//
+// DownloadStringCompletedEventArgs.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.IO;
+using System.ComponentModel;
+
+namespace Migo2.Net
+{
+    public class DownloadStringCompletedEventArgs : AsyncCompletedEventArgs
+    {
+        private readonly string result;
+
+        public string Result {
+            get {
+                RaiseExceptionIfNecessary ();
+                return result;
+            }
+        }
+
+        internal DownloadStringCompletedEventArgs (string result,
+                                                   Exception error,
+                                                   bool cancelled,
+                                                   object userState)
+            : base (error, cancelled, userState)
+        {
+            this.result = result;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/RemoteFileModifiedException.cs b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/RemoteFileModifiedException.cs
new file mode 100644
index 0000000..c43440e
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/RemoteFileModifiedException.cs
@@ -0,0 +1,59 @@
+//
+// RemoteFileModifiedException.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Net
+{
+    public class RemoteFileModifiedException : Exception
+    {
+        private readonly DateTime localFileMod;
+        private readonly DateTime remoteFileMod;
+
+        public DateTime LocalFileModified {
+            get {
+                return remoteFileMod;
+            }
+        }
+
+        public DateTime RemoteFileModified {
+            get {
+                return localFileMod;
+            }
+        }
+
+        public RemoteFileModifiedException (string message, DateTime localFileMod, DateTime remoteFileMod)
+            : base (message)
+        {
+            this.localFileMod = localFileMod;
+            this.remoteFileMod = remoteFileMod;
+        }
+
+        public RemoteFileModifiedException (string message) : this (message, DateTime.MinValue, DateTime.MinValue)
+        {
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/TransferStatusManager.cs b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/TransferStatusManager.cs
new file mode 100644
index 0000000..ce790a8
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/TransferStatusManager.cs
@@ -0,0 +1,213 @@
+//
+// TransferStatusManager.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Net
+{
+    internal sealed partial class TransferStatusManager
+    {
+        private int progress;
+        private long progressStep;
+        private long progressThisStep;
+
+        private long bytesReceived;
+        private long bytesReceivedPreviously;
+
+        private long totalBytes = -1;
+
+        private readonly object sync = new object ();
+
+        public event EventHandler<DownloadProgressChangedEventArgs> ProgressChanged;
+
+        public long BytesReceived {
+            get {
+                return bytesReceived;
+            }
+            set {
+                SetBytesReceived (value);
+            }
+        }
+
+        public long BytesReceivedPreviously {
+            get {
+                return bytesReceivedPreviously;
+            }
+            set {
+                if (value < 0) {
+                    throw new ArgumentOutOfRangeException ("BytesReceivedPreviously", "Must be > 0");
+                }
+
+                bytesReceivedPreviously = value;
+                progressThisStep += value;
+                UpdateProgress ();
+            }
+        }
+
+        public int Progress {
+            get {
+                return progress;
+            }
+        }
+
+        public object SyncRoot {
+            get {
+                return sync;
+            }
+        }
+
+        public long TotalBytesReceived {
+            get {
+                return bytesReceived + bytesReceivedPreviously;
+            }
+        }
+
+        public long TotalBytes {
+            get {
+                return totalBytes;
+            }
+            set {
+                SetTotalBytes (value);
+            }
+        }
+
+        public TransferStatusManager ()
+        {
+            Reset ();
+        }
+
+        public void AddBytes (long bytes)
+        {
+            if (bytes < 0) {
+                throw new ArgumentOutOfRangeException ("bytes cannot be less than 0");
+            }
+
+            lock (sync) {
+                bytesReceived += bytes;
+                progressThisStep += bytes;
+                bytesThisInterval += bytes;
+            }
+
+            UpdateProgress ();
+        }
+
+        public void Reset ()
+        {
+            lock (sync) {
+                progress = 0;
+
+                progressStep = 0;
+                progressThisStep = 0;
+
+                totalBytes = -1;
+                bytesReceived = 0;
+                bytesReceivedPreviously = 0;
+
+                ResetTransferRates ();
+            }
+
+            UpdateProgress ();
+        }
+
+        private void SetBytesReceived (long bytesReceived)
+        {
+            if (bytesReceived < 0) {
+                throw new ArgumentOutOfRangeException ("bytesReceived cannot be less than 0");
+            }
+
+            lock (sync) {
+                if (totalBytes > -1 && totalBytes < bytesReceived) {
+                    throw new ArgumentOutOfRangeException ("bytesReceived cannot be greater than TotalBytes");
+                }
+
+                progressThisStep = 0;
+                this.bytesReceived = bytesReceived;
+            }
+
+            UpdateProgress ();
+        }
+
+        private void SetTotalBytes (long totalBytes)
+        {
+            bool update = false;
+
+            if (totalBytes < -1 || totalBytes == 0) {
+                throw new ArgumentOutOfRangeException ("totalBytes cannot be less than -1 or equal to 0");
+            }
+
+            lock (sync) {
+                if (totalBytes != -1 && totalBytes < bytesReceived) {
+                    throw new ArgumentOutOfRangeException ("totalBytes cannot be less than bytesReceived");
+                }
+
+                if (this.totalBytes != totalBytes) {
+                    this.totalBytes = totalBytes;
+                    progressStep = (totalBytes / 100);
+                    update = true;
+                }
+            }
+
+            if (update) {
+                UpdateProgress ();
+            }
+        }
+
+        private void UpdateProgress ()
+        {
+            DownloadProgressChangedEventArgs args = null;
+
+            lock (sync) {
+                long totalBytesReceived = TotalBytesReceived;
+
+                if (totalBytes > 0 &&
+                    progressThisStep >= progressStep ||
+                    (totalBytesReceived == totalBytes && progress != 100)) {
+                    progress = Convert.ToInt32 ((totalBytesReceived * 100) / totalBytes);
+
+                    if (progress >= 0) {
+                        args = new DownloadProgressChangedEventArgs (progress, null, totalBytesReceived, totalBytes);
+                    }
+
+                    progressThisStep = 0;
+                }
+            }
+
+            if (args != null) {
+                OnProgressChanged (args);
+            }
+        }
+
+        private void OnProgressChanged (DownloadProgressChangedEventArgs args)
+        {
+            EventHandler <DownloadProgressChangedEventArgs>
+            handler = ProgressChanged;
+
+            if (handler != null) {
+                handler (this, args);
+            }
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/TransferStatusManager_Rate.cs b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/TransferStatusManager_Rate.cs
new file mode 100644
index 0000000..3b34d40
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Net/AsyncWebClient/TransferStatusManager_Rate.cs
@@ -0,0 +1,161 @@
+//
+// TransferStatusManager_Rate.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Timers;
+
+namespace Migo2.Net
+{
+    internal sealed partial class TransferStatusManager
+    {
+        private DateTime lastTick;
+
+        private long transferRate = -1;
+        private long transferRatePreviously = -1;
+
+        private long bytesThisInterval;
+        private int interval = (1500 * 1); // 1.5 seconds
+
+        private Timer statusTimer;
+        private bool enableTransferRateReporting;
+
+        public EventHandler<DownloadStatusUpdatedEventArgs> StatusUpdated;
+
+        public bool EnableStatusReporting {
+            get { return enableTransferRateReporting; }
+            set {
+                lock (sync) {
+                    if (value != enableTransferRateReporting) {
+                        enableTransferRateReporting = !enableTransferRateReporting;
+
+                        if (enableTransferRateReporting) {
+                            statusTimer = new Timer ();
+
+                            statusTimer.Elapsed += OnstatusTimerElapsedHandler;
+                            statusTimer.Interval = interval;
+                            statusTimer.Enabled = true;
+                            lastTick = DateTime.Now;
+                        } else {
+                            if (statusTimer != null) {
+                                statusTimer.Enabled = false;
+                                statusTimer.Elapsed -= OnstatusTimerElapsedHandler;
+                                statusTimer.Dispose ();
+                                statusTimer = null;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        public int Interval {
+            get { return interval; }
+            set {
+                lock (sync) {
+                    interval = value;
+
+                    if (statusTimer != null) {
+                        statusTimer.Interval = interval;
+                    }
+                }
+            }
+        }
+
+        public long TransferRate {
+            get {
+                lock (sync) {
+                    return transferRate;
+                }
+            }
+        }
+
+        private void ResetTransferRates ()
+        {
+            lock (sync) {
+                bytesThisInterval = 0;
+                transferRate = -1;
+                transferRatePreviously = -1;
+            }
+        }
+
+        private long CalculateTransferRate ()
+        {
+            long bytesPerSecond;
+
+            TimeSpan duration = (DateTime.Now - lastTick);
+            double secondsElapsed = duration.TotalSeconds;
+
+            if ((int)secondsElapsed == 0) {
+                return 0;
+            }
+
+            bytesPerSecond = (long) ((bytesThisInterval / secondsElapsed));
+
+            lastTick = DateTime.Now;
+            bytesThisInterval = 0;
+
+            return bytesPerSecond;
+        }
+
+        private void UpdateTransferRate ()
+        {
+            transferRate = CalculateTransferRate ();
+
+            if (transferRatePreviously == 0) {
+                if (transferRate > 0) {
+                    transferRatePreviously = transferRate;
+                    return;
+                }
+            }
+
+            if (transferRate != transferRatePreviously) {
+                transferRate = ((transferRate + transferRatePreviously) / 2);
+                transferRatePreviously = transferRate;
+            }
+        }
+
+        private void OnstatusTimerElapsedHandler (object source, System.Timers.ElapsedEventArgs e)
+        {
+            DownloadStatus status;
+
+            lock (sync) {
+                UpdateTransferRate ();
+                status = new DownloadStatus (progress, bytesReceived, totalBytes, TotalBytesReceived, transferRate);
+            }
+
+            OnStatusUpdated (status);
+        }
+
+        private void OnStatusUpdated (DownloadStatus status)
+        {
+            EventHandler<DownloadStatusUpdatedEventArgs> handler = StatusUpdated;
+
+            if (handler != null) {
+                handler (this, new DownloadStatusUpdatedEventArgs (status, null));
+            }
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Utils/Rfc822DateTime.cs b/src/Libraries/Migo2/Migo2.Utils/Rfc822DateTime.cs
new file mode 100644
index 0000000..211ac12
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Utils/Rfc822DateTime.cs
@@ -0,0 +1,199 @@
+//
+// Rfc822DateTime.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2006-09 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Text.RegularExpressions;
+
+namespace Migo2.Utils
+{
+    public static class Rfc822DateTime
+    {
+        private const string monthsStr =
+            "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|" +
+            "January|February|March|April|May|June|July|August|" +
+            "September|October|November|December";
+
+        private const string daysOfWeek =
+            "Mon|Tue|Wed|Thu|Fri|Sat|Sun|" +
+            "Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday";
+
+        private const string rfc822DTExp =
+            @"^(?<dayofweek>(" + daysOfWeek + "), )?" +
+            @"(?<day>\d\d?) " +
+            @"(?<month>" + monthsStr + ") " +
+            @"(?<year>\d\d(\d\d)?) " +
+            @"(?<hours>[0-2]?\d):(?<minutes>[0-5]\d)(:(?<seconds>[0-5]\d))?" +
+            @"( (?<timezone>[A-I]|[K-Z]|GMT|UT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|([+-]\d\d\d\d))$)?";
+
+        private static readonly string[] months;
+        private static readonly Regex rfc822DTRegex;
+
+        static Rfc822DateTime()
+        {
+            months = monthsStr.Split ('|');
+            rfc822DTRegex = new Regex (rfc822DTExp,
+                RegexOptions.Compiled | RegexOptions.IgnoreCase);
+        }
+
+        public static DateTime Parse (string dateTime)
+        {
+            if (dateTime == null) {
+                throw new ArgumentNullException ("dateTime");
+            }
+
+            Match m = rfc822DTRegex.Match (dateTime);
+
+            if (m.Success) {
+                DateTime ret;
+                GroupCollection groups = m.Groups;
+
+                int day     = Convert.ToInt32 (groups ["day"].Value);
+                int month   = MonthToInt32    (groups ["month"].Value);
+                int year    = Convert.ToInt32 (groups ["year"].Value);
+
+                int hours   = Convert.ToInt32 (groups ["hours"].Value);
+                int minutes = Convert.ToInt32 (groups ["minutes"].Value);
+
+                int seconds = 0;
+                string secondsStr = groups ["seconds"].Value;
+                string timeZone   = groups ["timezone"].Value;
+
+                if (secondsStr != String.Empty) {
+                    seconds = Convert.ToInt32 (secondsStr);
+                }
+
+                if (year < 100) {
+                    int curYear = DateTime.Now.Year;
+                    year = curYear - (curYear % 100) + year;
+                }
+
+                ret = new DateTime (year, month, day, hours, minutes, seconds);
+
+                if (timeZone != String.Empty) {
+                    ret -= ParseGmtOffset (timeZone);
+                }
+
+                return ret.ToLocalTime ();
+            }
+
+            throw new FormatException ("'dateTime' does not represent a valid RFC 822 date-time");
+        }
+
+        public static bool TryParse (string dateTime, out DateTime result)
+        {
+            bool ret = false;
+            result = DateTime.MinValue;
+
+            try {
+                result = Parse (dateTime);
+                ret = true;
+            } catch {}
+
+            return ret;
+        }
+
+        private static int MonthToInt32 (string month)
+        {
+            int i = 1;
+
+            foreach (string s in months) {
+                if (month == s) {
+                    break;
+                }
+
+                if (++i % 13 == 0) {
+                    i = 1;
+                }
+            }
+
+            return i;
+        }
+
+        private static TimeSpan ParseGmtOffset (string offset)
+        {
+            int offsetHours = 0;
+            int offsetMinutes = 0;
+
+            if (offset.Length == 5) {
+                offsetHours   = Convert.ToInt32 (offset.Substring (1,2));
+                offsetMinutes = Convert.ToInt32 (offset.Substring (3,2));
+
+                if (offset [0] == '-') {
+                    offsetHours   *= -1;
+                    offsetMinutes *= -1;
+                }
+            } else {
+                switch (offset)
+                {
+                    case "GMT": case "UT": break;
+                    case "EDT": offsetHours = -4; break;
+                    case "EST": case "CDT": offsetHours = -5; break;
+                    case "CST": case "MDT": offsetHours = -6; break;
+                    case "MST": case "PDT": offsetHours = -7; break;
+                    case "PST": offsetHours = -8; break;
+
+                    case "Z": offsetHours = 0;    break;
+
+                    case "A": offsetHours = -1;   break;
+                    case "B": offsetHours = -2;   break;
+                    case "C": offsetHours = -3;   break;
+                    case "D": offsetHours = -4;   break;
+                    case "E": offsetHours = -5;   break;
+                    case "F": offsetHours = -6;   break;
+                    case "G": offsetHours = -7;   break;
+                    case "H": offsetHours = -8;   break;
+                    case "I": offsetHours = -9;   break;
+
+                    // Q.  Why was 'J' left out of Z-Time?
+                    // A.  http://www.maybeck.com/ztime/
+
+                    // That's what I like about this job, you learn stuff.
+
+                    case "K": offsetHours = -10;  break;
+                    case "L": offsetHours = -11;  break;
+                    case "M": offsetHours = -12;  break;
+                    case "N": offsetHours = 1;    break;
+                    case "O": offsetHours = 2;    break;
+                    case "P": offsetHours = 3;    break;
+                    case "Q": offsetHours = 4;    break;
+                    case "R": offsetHours = 5;    break;
+                    case "S": offsetHours = 6;    break;
+                    case "T": offsetHours = 7;    break;
+                    case "U": offsetHours = 8;    break;
+                    case "V": offsetHours = 9;    break;
+                    case "W": offsetHours = 10;   break;
+                    case "X": offsetHours = 11;   break;
+                    case "Y": offsetHours = 12;   break;
+                }
+            }
+
+            return TimeSpan.FromTicks (
+                (offsetHours   * TimeSpan.TicksPerHour  ) +
+                (offsetMinutes * TimeSpan.TicksPerMinute)
+            );
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.Utils/UnitUtils.cs b/src/Libraries/Migo2/Migo2.Utils/UnitUtils.cs
new file mode 100644
index 0000000..07cf6f7
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Utils/UnitUtils.cs
@@ -0,0 +1,70 @@
+//
+// UnitUtils.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Migo2.Utils
+{
+    public static class UnitUtils
+    {
+        public static string ToString (long bytes)
+        {
+            if (bytes == -1) {
+                return String.Empty;
+            } else if (bytes < -1) {
+                throw new ArgumentException ("Must be >= 0", "bytes");
+            }
+
+            int divisor = 1;
+            string formatString = String.Empty;
+            string unit = String.Empty;
+
+            if (bytes >= 0 && bytes < 1000) {
+                unit = "B";
+                formatString = "{0:F0} {1}";
+            } else if (bytes > 1000 && bytes < 1000000) {
+                // Not all those who wander are lost.
+
+                // 1 kB == 1000 bytes.  1 KiB == 1024 bytes.
+                // Do not, change this unless you also want to change the abbreviations!
+
+                unit = "kB";
+                divisor = 1000;
+                formatString = "{0:F0} {1}";
+            } else if (bytes > 1000000 && bytes < 1000000000) {
+                unit = "MB";
+                divisor = 1000000;
+                formatString = "{0:F1} {1}";
+            } else if (bytes > 1000000000) {
+                unit = "GB";
+                divisor = 1000000000;
+                formatString = "{0:F2} {1}";
+            }
+
+            return String.Format (formatString, ((double)bytes/divisor), unit);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Libraries/Migo2/Migo2.Utils/XmlUtils.cs b/src/Libraries/Migo2/Migo2.Utils/XmlUtils.cs
new file mode 100644
index 0000000..6174ef2
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.Utils/XmlUtils.cs
@@ -0,0 +1,102 @@
+//
+// XmlUtils.cs
+//
+// Author:
+//       Mike Urbanski <michael c urbanski gmail com>
+//
+// Copyright (c) 2009 Michael C. Urbanski
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Xml;
+
+namespace Migo2.Utils
+{
+    public static class XmlUtils
+    {
+        public static string GetXmlNodeText (XmlNode node, string tag)
+        {
+            return GetXmlNodeText (node, tag, null);
+        }
+
+        public static string GetXmlNodeText (XmlNode node, string tag, XmlNamespaceManager mgr)
+        {
+            XmlNode n = node.SelectSingleNode (tag, mgr);
+            return (n == null) ? null : n.InnerText.Trim ();
+        }
+
+        public static int GetInt32 (XmlNode node, string tag)
+        {
+            return GetInt32 (node, tag, null);
+        }
+
+        public static int GetInt32 (XmlNode node, string tag, XmlNamespaceManager mgr)
+        {
+            int ret = 0;
+            string result = GetXmlNodeText (node, tag, mgr);
+
+            if (!String.IsNullOrEmpty (result)) {
+                Int32.TryParse (result, out ret);
+            }
+
+            return ret;
+        }
+
+        public static long GetInt64 (XmlNode node, string tag)
+        {
+            return GetInt64 (node, tag, null);
+        }
+
+        public static long GetInt64 (XmlNode node, string tag, XmlNamespaceManager mgr)
+        {
+            long ret = 0;
+            string result = GetXmlNodeText (node, tag, mgr);
+
+            if (!String.IsNullOrEmpty (result)) {
+                Int64.TryParse (result, out ret);
+            }
+
+            return ret;
+        }
+
+        public static DateTime GetRfc822DateTime (XmlNode node, string tag)
+        {
+            return GetRfc822DateTime (node, tag, null);
+        }
+
+        public static DateTime GetRfc822DateTime (XmlNode node, string tag, XmlNamespaceManager mgr)
+        {
+            DateTime ret;
+            string result = GetXmlNodeText (node, tag, mgr);
+
+            if (!String.IsNullOrEmpty (result)) {
+                if (Rfc822DateTime.TryParse (result, out ret)) {
+                    return ret;
+                }
+
+                if (DateTime.TryParse (result, out ret)) {
+                    return ret;
+                }
+            }
+
+            return DateTime.MinValue;
+        }
+    }
+}
diff --git a/src/Libraries/Migo2/Migo2.csproj b/src/Libraries/Migo2/Migo2.csproj
new file mode 100644
index 0000000..e142b4c
--- /dev/null
+++ b/src/Libraries/Migo2/Migo2.csproj
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"; ToolsVersion="3.5">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>9.0.21022</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{FC311410-8638-4A66-A8A5-1E900CDC6C7B}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AssemblyName>Migo2</AssemblyName>
+    <TargetFrameworkVersion>v2.0</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>none</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Web" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Migo2.Async\CommandQueue\CommandDelegate.cs" />
+    <Compile Include="Migo2.Async\CommandQueue\CommandQueue.cs" />
+    <Compile Include="Migo2.Async\CommandQueue\CommandWrapper.cs" />
+    <Compile Include="Migo2.Async\CommandQueue\EventWrapper.cs" />
+    <Compile Include="Migo2.Async\CommandQueue\ICommand.cs" />
+    <Compile Include="Migo2.Async\Task\CancellationType.cs" />
+    <Compile Include="Migo2.Async\Task\Task.cs" />
+    <Compile Include="Migo2.Async\Task\TaskCompletedEventArgs.cs" />
+    <Compile Include="Migo2.Async\Task\TaskEventArgs.cs" />
+    <Compile Include="Migo2.Async\Task\TaskState.cs" />
+    <Compile Include="Migo2.Async\Task\TaskStateChangedEventArgs.cs" />
+    <Compile Include="Migo2.Async\Task\IWaitableTask.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\EventArgs\GroupStatusChangedEventArgs.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\EventArgs\ManipulatedEventArgs.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\EventArgs\ReorderedEventArgs.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\EventArgs\TaskAddedEventArgs.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\EventArgs\TaskProgressChangedEventArgs.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\EventArgs\TaskRemovedEventArgs.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\GroupProgressManager.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\GroupStatusManager.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\TaskGroup.cs" />
+    <Compile Include="Migo2.Async\TaskGroup\TaskGroup_Collection.cs" />
+    <Compile Include="Migo2.Async\AsyncStateManager.cs" />
+    <Compile Include="Migo2.Collections\Pair.cs" />
+    <Compile Include="Migo2.DownloadService\DownloadStatusManager.cs" />
+    <Compile Include="Migo2.DownloadService\DownloadTaskStatusUpdatedEventArgs.cs" />
+    <Compile Include="Migo2.DownloadService\HttpDownloadGroup.cs" />
+    <Compile Include="Migo2.DownloadService\HttpDownloadGroupStatusChangedEventArgs.cs" />
+    <Compile Include="Migo2.DownloadService\HttpDownloadManager.cs" />
+    <Compile Include="Migo2.DownloadService\HttpFileDownloadErrors.cs" />
+    <Compile Include="Migo2.DownloadService\HttpFileDownloadTask.cs" />
+    <Compile Include="Migo2.Net\AsyncWebClient\AsyncWebClient.cs" />
+    <Compile Include="Migo2.Net\AsyncWebClient\DownloadDataCompletedEventArgs.cs" />
+    <Compile Include="Migo2.Net\AsyncWebClient\DownloadProgressChangedEventArgs.cs" />
+    <Compile Include="Migo2.Net\AsyncWebClient\DownloadStatus.cs" />
+    <Compile Include="Migo2.Net\AsyncWebClient\DownloadStatusUpdatedEventArgs.cs" />
+    <Compile Include="Migo2.Net\AsyncWebClient\DownloadStringCompletedEventArgs.cs" />
+    <Compile Include="Migo2.Net\AsyncWebClient\RemoteFileModifiedException.cs" />
+    <Compile Include="Migo2.Net\AsyncWebClient\TransferStatusManager.cs" />
+    <Compile Include="Migo2.Net\AsyncWebClient\TransferStatusManager_Rate.cs" />
+    <Compile Include="Migo2.Utils\Rfc822DateTime.cs" />
+    <Compile Include="Migo2.Utils\UnitUtils.cs" />
+    <Compile Include="Migo2.Utils\XmlUtils.cs" />
+    <Compile Include="Migo2.Collections\OrderComparer.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ProjectExtensions>
+    <MonoDevelop>
+      <Properties>
+        <MonoDevelop.Autotools.MakefileInfo IntegrationEnabled="true" RelativeMakefileName="Makefile.am" IsAutotoolsProject="true" RelativeConfigureInPath="../../..">
+          <BuildFilesVar Sync="true" Name="SOURCES" />
+          <DeployFilesVar />
+          <ResourcesVar />
+          <OthersVar />
+          <GacRefVar />
+          <AsmRefVar />
+          <ProjectRefVar />
+          <MessageRegex Name="Vala" />
+        </MonoDevelop.Autotools.MakefileInfo>
+      </Properties>
+    </MonoDevelop>
+  </ProjectExtensions>
+</Project>
\ No newline at end of file
diff --git a/src/Libraries/Migo2/README.png b/src/Libraries/Migo2/README.png
new file mode 100644
index 0000000..5550fd9
Binary files /dev/null and b/src/Libraries/Migo2/README.png differ



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]