[banshee/watcher] Patch 144873 from bgo#385965



commit b7dc2f9fa4c37368c07a585038d398efce2b1947
Author: Alexander Kojevnikov <alexander kojevnikov com>
Date:   Tue Nov 17 10:35:46 2009 +1100

    Patch 144873 from bgo#385965

 Banshee.sln                                        |    7 +
 build/build.environment.mk                         |    1 +
 configure.ac                                       |    1 +
 .../Banshee.Sources/PrimarySource.cs               |    2 +-
 .../Banshee.LibraryWatcher.addin.xml               |   22 +
 .../Banshee.LibraryWatcher.csproj                  |   85 +++
 .../FileSystemWatcherProxy.cs                      |  146 +++++
 .../Banshee.LibraryWatcher/IO/FileAction.cs        |   39 ++
 .../Banshee.LibraryWatcher/IO/FileSystemWatcher.cs |  541 +++++++++++++++++
 .../Banshee.LibraryWatcher/IO/IFileWatcher.cs      |   37 ++
 .../Banshee.LibraryWatcher/IO/InotifyWatcher.cs    |  624 ++++++++++++++++++++
 .../Banshee.LibraryWatcher/IO/SearchPattern.cs     |  225 +++++++
 .../LibraryWatcherService.cs                       |  120 ++++
 .../Banshee.LibraryWatcher/SourceWatcher.cs        |  293 +++++++++
 src/Extensions/Banshee.LibraryWatcher/Makefile.am  |   19 +
 src/Extensions/Makefile.am                         |    2 +
 .../Hyena/Hyena.Query/StringQueryValue.cs          |    4 +-
 src/Libraries/Hyena/Hyena/StringUtil.cs            |    9 +
 18 files changed, 2173 insertions(+), 4 deletions(-)
---
diff --git a/Banshee.sln b/Banshee.sln
index 3fb6a39..264e065 100644
--- a/Banshee.sln
+++ b/Banshee.sln
@@ -63,6 +63,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.NowPlaying", "src\E
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.Lastfm", "src\Extensions\Banshee.Lastfm\Banshee.Lastfm.csproj", "{02FD8195-9796-4AF5-A9D2-D310721963F4}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.LibraryWatcher", "src\Extensions\Banshee.LibraryWatcher\Banshee.LibraryWatcher.csproj", "{49CA3F27-0BB6-428d-8B3A-20232493652E}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.Wikipedia", "src\Extensions\Banshee.Wikipedia\Banshee.Wikipedia.csproj", "{BF5D1722-269B-452E-B577-AEBA0CB894BA}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.PlayerMigration", "src\Extensions\Banshee.PlayerMigration\Banshee.PlayerMigration.csproj", "{0AB92BF8-3A25-46AD-9748-1236471E9408}"
@@ -241,6 +243,8 @@ Global
 		{EABA3019-7539-4430-9935-D36CEA96F250}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{F38B53BA-8F85-4DC6-9B94-029C1CF96F24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{F38B53BA-8F85-4DC6-9B94-029C1CF96F24}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{49CA3F27-0BB6-428d-8B3A-20232493652E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{49CA3F27-0BB6-428d-8B3A-20232493652E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{FCC1AE87-E10B-4B47-8ADE-D5F447E48518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{FCC1AE87-E10B-4B47-8ADE-D5F447E48518}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{BB1D1D81-7A74-4183-B7B1-3E78B32D42F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
@@ -309,6 +313,8 @@ Global
 		{0130499B-8A93-4CD9-8F3C-593B231609C7}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
 		{F38B53BA-8F85-4DC6-9B94-029C1CF96F24}.Windows|Any CPU.Build.0 = Windows|Any CPU
 		{F38B53BA-8F85-4DC6-9B94-029C1CF96F24}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
+		{49CA3F27-0BB6-428d-8B3A-20232493652E}.Windows|Any CPU.Build.0 = Windows|Any CPU
+		{49CA3F27-0BB6-428d-8B3A-20232493652E}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
 		{FCC1AE87-E10B-4B47-8ADE-D5F447E48518}.Windows|Any CPU.Build.0 = Windows|Any CPU
 		{FCC1AE87-E10B-4B47-8ADE-D5F447E48518}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
 		{46AD1892-C5D3-4696-BA40-FBF7F4CE2B39}.Windows|Any CPU.Build.0 = Windows|Any CPU
@@ -391,6 +397,7 @@ Global
 		{16FB0D3A-53FA-4B8E-B02B-4AF66E87829A} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{0130499B-8A93-4CD9-8F3C-593B231609C7} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{F38B53BA-8F85-4DC6-9B94-029C1CF96F24} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
+		{49CA3F27-0BB6-428d-8B3A-20232493652E} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{FCC1AE87-E10B-4B47-8ADE-D5F447E48518} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{46AD1892-C5D3-4696-BA40-FBF7F4CE2B39} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{6FF6F049-9DAB-48A7-BC4B-F7F3ED0EBA63} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
diff --git a/build/build.environment.mk b/build/build.environment.mk
index b4d71ae..e766618 100644
--- a/build/build.environment.mk
+++ b/build/build.environment.mk
@@ -126,6 +126,7 @@ REF_EXTENSION_DAAP = $(LINK_BANSHEE_THICKCLIENT_DEPS) $(LINK_ICSHARP_ZIP_LIB) $(
 REF_EXTENSION_FILESYSTEMQUEUE = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_INTERNETRADIO = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_INTERNETARCHIVE = $(LINK_BANSHEE_THICKCLIENT_DEPS)
+REF_EXTENSION_LIBRARYWATCHER = $(LINK_BANSHEE_SERVICES_DEPS)
 REF_EXTENSION_MINIMODE = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_MOBLIN = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_MULTIMEDIAKEYS = $(LINK_BANSHEE_SERVICES_DEPS)
diff --git a/configure.ac b/configure.ac
index 5b78f4e..dd242d1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -287,6 +287,7 @@ src/Extensions/Banshee.FileSystemQueue/Makefile
 src/Extensions/Banshee.InternetArchive/Makefile
 src/Extensions/Banshee.InternetRadio/Makefile
 src/Extensions/Banshee.Lastfm/Makefile
+src/Extensions/Banshee.LibraryWatcher/Makefile
 src/Extensions/Banshee.MiniMode/Makefile
 src/Extensions/Banshee.Moblin/Makefile
 src/Extensions/Banshee.MultimediaKeys/Makefile
diff --git a/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs b/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs
index bdab989..b9f43a1 100644
--- a/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs
+++ b/src/Core/Banshee.Services/Banshee.Sources/PrimarySource.cs
@@ -335,7 +335,7 @@ namespace Banshee.Sources
             OnTracksChanged ();
         }
 
-        internal void NotifyTracksDeleted ()
+        public void NotifyTracksDeleted ()
         {
             OnTracksDeleted ();
         }
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.addin.xml b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.addin.xml
new file mode 100644
index 0000000..421c7d8
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.addin.xml
@@ -0,0 +1,22 @@
+<Addin
+    id="Banshee.LibraryWatcher"
+    version="1.0"
+    compatVersion="1.0"
+    copyright="© 2008-2009 Christian Martellini, Alexander Hixon, Alexander Kojevnikov. Licensed under the MIT X11 license."
+    name="Library Watcher"
+    category="Hardware"
+    description="Automatically update music and video libraries"
+    author="Christian Martellini, Alexander Hixon, Alexander Kojevnikov"
+    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.LibraryWatcher.LibraryWatcherService"/>
+  </Extension>
+
+</Addin>
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.csproj b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.csproj
new file mode 100644
index 0000000..ae55b3a
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher.csproj
@@ -0,0 +1,85 @@
+<?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>8.0.50727</ProductVersion>
+    <ProjectGuid>{49CA3F27-0BB6-428d-8B3A-20232493652E}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <UseParentDirectoryAsNamespace>true</UseParentDirectoryAsNamespace>
+    <AssemblyName>Banshee.LibraryWatcher</AssemblyName>
+    <SchemaVersion>2.0</SchemaVersion>
+    <TargetFrameworkVersion>v2.0</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>..\..\..\bin</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
+    <CustomCommands>
+      <CustomCommands>
+        <Command type="Build" command="make" workingdir="${SolutionDir}" />
+        <Command type="Execute" command="make run" workingdir="${SolutionDir}" />
+      </CustomCommands>
+    </CustomCommands>
+    <AssemblyKeyFile>.</AssemblyKeyFile>
+  </PropertyGroup>
+  <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\Hyena.csproj">
+      <Project>{95374549-9553-4C1E-9D89-667755F90E12}</Project>
+      <Name>Hyena</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Core\Banshee.ThickClient\Banshee.ThickClient.csproj">
+      <Project>{AC839523-7BDF-4AB6-8115-E17921B96EC6}</Project>
+      <Name>Banshee.ThickClient</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="Mono.Addins, Version=0.4.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756" />
+    <Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Banshee.LibraryWatcher.addin.xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Banshee.LibraryWatcher\LibraryWatcherService.cs" />
+    <Compile Include="Banshee.LibraryWatcher\SourceWatcher.cs" />
+    <Compile Include="Banshee.LibraryWatcher\IO\FileSystemWatcher.cs" />
+    <Compile Include="Banshee.LibraryWatcher\IO\InotifyWatcher.cs" />
+    <Compile Include="Banshee.LibraryWatcher\IO\IFileWatcher.cs" />
+    <Compile Include="Banshee.LibraryWatcher\IO\FileAction.cs" />
+    <Compile Include="Banshee.LibraryWatcher\IO\SearchPattern.cs" />
+    <Compile Include="Banshee.LibraryWatcher\FileSystemWatcherProxy.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ProjectExtensions>
+    <MonoDevelop>
+      <Properties>
+        <MonoDevelop.Autotools.MakefileInfo IntegrationEnabled="true" RelativeMakefileName="./Makefile.am">
+          <BuildFilesVar Sync="true" Name="SOURCES" />
+          <DeployFilesVar />
+          <ResourcesVar Sync="true" Name="RESOURCES" />
+          <OthersVar />
+          <GacRefVar />
+          <AsmRefVar />
+          <ProjectRefVar />
+        </MonoDevelop.Autotools.MakefileInfo>
+      </Properties>
+    </MonoDevelop>
+  </ProjectExtensions>
+  <ItemGroup>
+    <Folder Include="Banshee.LibraryWatcher\IO\" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/FileSystemWatcherProxy.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/FileSystemWatcherProxy.cs
new file mode 100644
index 0000000..2193702
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/FileSystemWatcherProxy.cs
@@ -0,0 +1,146 @@
+//
+// FileSystemWatcherProxy.cs
+//
+// Authors:
+//   Alexander Kojevnikov <alexander kojevnikov com>
+//
+// Copyright (C) 2009 Alexander Kojevnikov
+//
+// 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.LibraryWatcher
+{
+    public class FileSystemWatcherProxy : IDisposable
+    {
+        private readonly Banshee.LibraryWatcher.IO.FileSystemWatcher watcher_fixed;
+        private readonly System.IO.FileSystemWatcher watcher;
+
+        public FileSystemWatcherProxy(string path)
+        {
+            // FIXME: There must be a better way to check if we are running under Linux.
+            if (Environment.OSVersion.Platform == PlatformID.Unix &&
+                Environment.OSVersion.Version.ToString ().StartsWith ("2.6.")) {
+                watcher_fixed = new Banshee.LibraryWatcher.IO.FileSystemWatcher (path);
+            } else {
+                watcher = new System.IO.FileSystemWatcher (path);
+            }
+        }
+
+        public void Dispose ()
+        {
+            if (watcher_fixed != null) {
+                watcher_fixed.Dispose ();
+            } else {
+                watcher.Dispose ();
+            }
+        }
+
+        public bool IncludeSubdirectories {
+            set {
+                if (watcher_fixed != null) {
+                    watcher_fixed.IncludeSubdirectories = value;
+                } else {
+                    watcher.IncludeSubdirectories = value;
+                }
+            }
+        }
+
+        public event System.IO.FileSystemEventHandler Changed {
+            add {
+                if (watcher_fixed != null) {
+                    watcher_fixed.Changed += value;
+                } else {
+                    watcher.Changed += value;
+                }
+            }
+            remove {
+                if (watcher_fixed != null) {
+                    watcher_fixed.Changed -= value;
+                } else {
+                    watcher.Changed -= value;
+                }
+            }
+        }
+
+        public event System.IO.FileSystemEventHandler Created {
+            add {
+                if (watcher_fixed != null) {
+                    watcher_fixed.Created += value;
+                } else {
+                    watcher.Created += value;
+                }
+            }
+            remove {
+                if (watcher_fixed != null) {
+                    watcher_fixed.Created -= value;
+                } else {
+                    watcher.Created -= value;
+                }
+            }
+        }
+
+        public event System.IO.FileSystemEventHandler Deleted {
+            add {
+                if (watcher_fixed != null) {
+                    watcher_fixed.Deleted += value;
+                } else {
+                    watcher.Deleted += value;
+                }
+            }
+            remove {
+                if (watcher_fixed != null) {
+                    watcher_fixed.Deleted -= value;
+                } else {
+                    watcher.Deleted -= value;
+                }
+            }
+        }
+
+        public event System.IO.RenamedEventHandler Renamed {
+            add {
+                if (watcher_fixed != null) {
+                    watcher_fixed.Renamed += value;
+                } else {
+                    watcher.Renamed += value;
+                }
+            }
+            remove {
+                if (watcher_fixed != null) {
+                    watcher_fixed.Renamed -= value;
+                } else {
+                    watcher.Renamed -= value;
+                }
+            }
+        }
+
+        public bool EnableRaisingEvents {
+            set {
+                if (watcher_fixed != null) {
+                    watcher_fixed.EnableRaisingEvents = value;
+                } else {
+                    watcher.EnableRaisingEvents = value;
+                }
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileAction.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileAction.cs
new file mode 100644
index 0000000..8d64a87
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileAction.cs
@@ -0,0 +1,39 @@
+// 
+// System.IO.FileAction.cs
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (c) 2004 Novell, Inc. (http://www.novell.com)
+//
+
+//
+// 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.LibraryWatcher.IO {
+	enum FileAction {
+		Added = 1,
+		Removed = 2,
+		Modified = 3,
+		RenamedOldName = 4,
+		RenamedNewName = 5
+	}
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileSystemWatcher.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileSystemWatcher.cs
new file mode 100644
index 0000000..f4eba39
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/FileSystemWatcher.cs
@@ -0,0 +1,541 @@
+// 
+// System.IO.FileSystemWatcher.cs
+//
+// Authors:
+// 	Tim Coleman (tim timcoleman com)
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// Copyright (C) Tim Coleman, 2002 
+// (c) 2003 Ximian, Inc. (http://www.ximian.com)
+// Copyright (C) 2004, 2006 Novell, Inc (http://www.novell.com)
+//
+
+//
+// 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;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security.Permissions;
+using System.Threading;
+
+namespace Banshee.LibraryWatcher.IO {
+	[DefaultEvent("Changed")]
+#if NET_2_0
+	[IODescription ("")]
+#endif
+	public class FileSystemWatcher : Component, ISupportInitialize {
+
+		#region Fields
+
+		bool enableRaisingEvents;
+		string filter;
+		bool includeSubdirectories;
+		int internalBufferSize;
+		NotifyFilters notifyFilter;
+		string path;
+		string fullpath;
+		ISynchronizeInvoke synchronizingObject;
+		WaitForChangedResult lastData;
+		bool waiting;
+		SearchPattern2 pattern;
+		bool disposed;
+		string mangledFilter;
+		static IFileWatcher watcher;
+		static object lockobj = new object ();
+
+		#endregion // Fields
+
+		#region Constructors
+
+		public FileSystemWatcher ()
+		{
+			this.notifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
+			this.enableRaisingEvents = false;
+			this.filter = "*.*";
+			this.includeSubdirectories = false;
+			this.internalBufferSize = 8192;
+			this.path = "";
+			InitWatcher ();
+		}
+
+		public FileSystemWatcher (string path)
+			: this (path, "*.*")
+		{
+		}
+
+		public FileSystemWatcher (string path, string filter)
+		{
+			if (path == null)
+				throw new ArgumentNullException ("path");
+
+			if (filter == null)
+				throw new ArgumentNullException ("filter");
+
+			if (path == String.Empty)
+				throw new ArgumentException ("Empty path", "path");
+
+			if (!Directory.Exists (path))
+				throw new ArgumentException ("Directory does not exists", "path");
+
+			this.enableRaisingEvents = false;
+			this.filter = filter;
+			this.includeSubdirectories = false;
+			this.internalBufferSize = 8192;
+			this.notifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
+			this.path = path;
+			this.synchronizingObject = null;
+			InitWatcher ();
+		}
+
+		[EnvironmentPermission (SecurityAction.Assert, Read="MONO_MANAGED_WATCHER")]
+		void InitWatcher ()
+		{
+			lock (lockobj) {
+				if (watcher != null)
+					return;
+
+				/*string managed = Environment.GetEnvironmentVariable ("MONO_MANAGED_WATCHER");
+				int mode = 0;
+				if (managed == null)
+					mode = InternalSupportsFSW ();
+				
+				bool ok = false;
+				switch (mode) {
+				case 1: // windows
+					ok = DefaultWatcher.GetInstance (out watcher);
+					//ok = WindowsWatcher.GetInstance (out watcher);
+					break;
+				case 2: // libfam
+					ok = FAMWatcher.GetInstance (out watcher, false);
+					break;
+				case 3: // kevent
+					ok = KeventWatcher.GetInstance (out watcher);
+					break;
+				case 4: // libgamin
+					ok = FAMWatcher.GetInstance (out watcher, true);
+					break;
+				case 5: // inotify
+					ok = InotifyWatcher.GetInstance (out watcher, true);
+					break;
+				}
+
+				if (mode == 0 || !ok) {
+					if (String.Compare (managed, "disabled", true) == 0)
+						NullFileWatcher.GetInstance (out watcher);
+					else
+						DefaultWatcher.GetInstance (out watcher);
+				}*/
+                InotifyWatcher.GetInstance (out watcher, true);
+
+				ShowWatcherInfo ();
+			}
+		}
+
+		[Conditional ("DEBUG"), Conditional ("TRACE")]
+		void ShowWatcherInfo ()
+		{
+			Console.WriteLine ("Watcher implementation: {0}", watcher != null ? watcher.GetType ().ToString () : "<none>");
+		}
+		
+		#endregion // Constructors
+
+		#region Properties
+
+		/* If this is enabled, we Pulse this instance */
+		internal bool Waiting {
+			get { return waiting; }
+			set { waiting = value; }
+		}
+
+		internal string MangledFilter {
+			get {
+				if (filter != "*.*")
+					return filter;
+
+				if (mangledFilter != null)
+					return mangledFilter;
+
+				string filterLocal = "*.*";
+				//if (!(watcher.GetType () == typeof (WindowsWatcher)))
+					filterLocal = "*";
+
+				return filterLocal;
+			}
+		}
+
+		internal SearchPattern2 Pattern {
+			get {
+				if (pattern == null) {
+					pattern = new SearchPattern2 (MangledFilter);
+				}
+				return pattern;
+			}
+		}
+
+		internal string FullPath {
+			get {
+				if (fullpath == null) {
+					if (path == null || path == "")
+						fullpath = Environment.CurrentDirectory;
+					else
+						fullpath = System.IO.Path.GetFullPath (path);
+				}
+
+				return fullpath;
+			}
+		}
+
+		[DefaultValue(false)]
+		[IODescription("Flag to indicate if this instance is active")]
+		public bool EnableRaisingEvents {
+			get { return enableRaisingEvents; }
+			set {
+				if (value == enableRaisingEvents)
+					return; // Do nothing
+
+				enableRaisingEvents = value;
+				if (value) {
+					Start ();
+				} else {
+					Stop ();
+				}
+			}
+		}
+
+		[DefaultValue("*.*")]
+		[IODescription("File name filter pattern")]
+		//[RecommendedAsConfigurable(true)]
+		//[TypeConverter ("System.Diagnostics.Design.StringValueConverter, " + Consts.AssemblySystem_Design)]
+		public string Filter {
+			get { return filter; }
+			set {
+				if (value == null || value == "")
+					value = "*.*";
+
+				if (filter != value) {
+					filter = value;
+					pattern = null;
+					mangledFilter = null;
+				}
+			}
+		}
+
+		[DefaultValue(false)]
+		[IODescription("Flag to indicate we want to watch subdirectories")]
+		public bool IncludeSubdirectories {
+			get { return includeSubdirectories; }
+			set {
+				if (includeSubdirectories == value)
+					return;
+
+				includeSubdirectories = value;
+				if (value && enableRaisingEvents) {
+					Stop ();
+					Start ();
+				}
+			}
+		}
+
+		[Browsable(false)]
+		[DefaultValue(8192)]
+		public int InternalBufferSize {
+			get { return internalBufferSize; }
+			set {
+				if (internalBufferSize == value)
+					return;
+
+				if (value < 4196)
+					value = 4196;
+
+				internalBufferSize = value;
+				if (enableRaisingEvents) {
+					Stop ();
+					Start ();
+				}
+			}
+		}
+
+		[DefaultValue(NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite)]
+		[IODescription("Flag to indicate which change event we want to monitor")]
+		public NotifyFilters NotifyFilter {
+			get { return notifyFilter; }
+			set {
+				if (notifyFilter == value)
+					return;
+					
+				notifyFilter = value;
+				if (enableRaisingEvents) {
+					Stop ();
+					Start ();
+				}
+			}
+		}
+
+		[DefaultValue("")]
+		[IODescription("The directory to monitor")]
+		//[RecommendedAsConfigurable(true)]
+		//[TypeConverter ("System.Diagnostics.Design.StringValueConverter, " + Consts.AssemblySystem_Design)]
+		//[Editor ("System.Diagnostics.Design.FSWPathEditor, " + Consts.AssemblySystem_Design, "System.Drawing.Design.UITypeEditor, " + Consts.AssemblySystem_Drawing)]
+		public string Path {
+			get { return path; }
+			set {
+				if (path == value)
+					return;
+
+				bool exists = false;
+				Exception exc = null;
+
+				try {
+					exists = Directory.Exists (value);
+				} catch (Exception e) {
+					exc = e;
+				}
+
+				if (exc != null)
+					throw new ArgumentException ("Invalid directory name", "value", exc);
+
+				if (!exists)
+					throw new ArgumentException ("Directory does not exists", "value");
+
+				path = value;
+				fullpath = null;
+				if (enableRaisingEvents) {
+					Stop ();
+					Start ();
+				}
+			}
+		}
+
+		[Browsable(false)]
+		public override ISite Site {
+			get { return base.Site; }
+			set { base.Site = value; }
+		}
+
+		[DefaultValue(null)]
+		[IODescription("The object used to marshal the event handler calls resulting from a directory change")]
+#if NET_2_0
+		[Browsable (false)]
+#endif
+		public ISynchronizeInvoke SynchronizingObject {
+			get { return synchronizingObject; }
+			set { synchronizingObject = value; }
+		}
+
+		#endregion // Properties
+
+		#region Methods
+	
+		public void BeginInit ()
+		{
+			// Not necessary in Mono
+		}
+
+		protected override void Dispose (bool disposing)
+		{
+			if (!disposed) {
+				disposed = true;
+				Stop ();
+			}
+
+			base.Dispose (disposing);
+		}
+
+		~FileSystemWatcher ()
+		{
+			disposed = true;
+			Stop ();
+		}
+		
+		public void EndInit ()
+		{
+			// Not necessary in Mono
+		}
+
+		enum EventType {
+			FileSystemEvent,
+			ErrorEvent,
+			RenameEvent
+		}
+		private void RaiseEvent (Delegate ev, EventArgs arg, EventType evtype)
+		{
+			if (ev == null)
+				return;
+
+			if (synchronizingObject == null) {
+				switch (evtype) {
+				case EventType.RenameEvent:
+					((RenamedEventHandler)ev).BeginInvoke (this, (RenamedEventArgs) arg, null, null);
+					break;
+				case EventType.ErrorEvent:
+					((ErrorEventHandler)ev).BeginInvoke (this, (ErrorEventArgs) arg, null, null);
+					break;
+				case EventType.FileSystemEvent:
+					((FileSystemEventHandler)ev).BeginInvoke (this, (FileSystemEventArgs) arg, null, null);
+					break;
+				}
+				return;
+			}
+			
+			synchronizingObject.BeginInvoke (ev, new object [] {this, arg});
+		}
+
+		protected void OnChanged (FileSystemEventArgs e)
+		{
+			RaiseEvent (Changed, e, EventType.FileSystemEvent);
+		}
+
+		protected void OnCreated (FileSystemEventArgs e)
+		{
+			RaiseEvent (Created, e, EventType.FileSystemEvent);
+		}
+
+		protected void OnDeleted (FileSystemEventArgs e)
+		{
+			RaiseEvent (Deleted, e, EventType.FileSystemEvent);
+		}
+
+		protected void OnError (ErrorEventArgs e)
+		{
+			RaiseEvent (Error, e, EventType.ErrorEvent);
+		}
+
+		protected void OnRenamed (RenamedEventArgs e)
+		{
+			RaiseEvent (Renamed, e, EventType.RenameEvent);
+		}
+
+		public WaitForChangedResult WaitForChanged (WatcherChangeTypes changeType)
+		{
+			return WaitForChanged (changeType, Timeout.Infinite);
+		}
+
+		public WaitForChangedResult WaitForChanged (WatcherChangeTypes changeType, int timeout)
+		{
+			WaitForChangedResult result = new WaitForChangedResult ();
+			bool prevEnabled = EnableRaisingEvents;
+			if (!prevEnabled)
+				EnableRaisingEvents = true;
+
+			bool gotData;
+			lock (this) {
+				waiting = true;
+				gotData = Monitor.Wait (this, timeout);
+				if (gotData)
+					result = this.lastData;
+			}
+
+			EnableRaisingEvents = prevEnabled;
+			if (!gotData)
+				result.TimedOut = true;
+
+			return result;
+		}
+
+		internal void DispatchEvents (FileAction act, string filename, ref RenamedEventArgs renamed)
+		{
+			if (waiting) {
+				lastData = new WaitForChangedResult ();
+			}
+
+			switch (act) {
+			case FileAction.Added:
+				lastData.Name = filename;
+				lastData.ChangeType = WatcherChangeTypes.Created;
+				OnCreated (new FileSystemEventArgs (WatcherChangeTypes.Created, path, filename));
+				break;
+			case FileAction.Removed:
+				lastData.Name = filename;
+				lastData.ChangeType = WatcherChangeTypes.Deleted;
+				OnDeleted (new FileSystemEventArgs (WatcherChangeTypes.Deleted, path, filename));
+				break;
+			case FileAction.Modified:
+				lastData.Name = filename;
+				lastData.ChangeType = WatcherChangeTypes.Changed;
+				OnChanged (new FileSystemEventArgs (WatcherChangeTypes.Changed, path, filename));
+				break;
+			case FileAction.RenamedOldName:
+				if (renamed != null) {
+					OnRenamed (renamed);
+				}
+				lastData.OldName = filename;
+				lastData.ChangeType = WatcherChangeTypes.Renamed;
+				renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, path, filename, "");
+				break;
+			case FileAction.RenamedNewName:
+				lastData.Name = filename;
+				lastData.ChangeType = WatcherChangeTypes.Renamed;
+				if (renamed == null) {
+					renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, path, "", filename);
+				}
+				OnRenamed (renamed);
+				renamed = null;
+				break;
+			default:
+				break;
+			}
+		}
+
+		void Start ()
+		{
+			watcher.StartDispatching (this);
+		}
+
+		void Stop ()
+		{
+			watcher.StopDispatching (this);
+		}
+		#endregion // Methods
+
+		#region Events and Delegates
+
+		[IODescription("Occurs when a file/directory change matches the filter")]
+		public event FileSystemEventHandler Changed;
+
+		[IODescription("Occurs when a file/directory creation matches the filter")]
+		public event FileSystemEventHandler Created;
+
+		[IODescription("Occurs when a file/directory deletion matches the filter")]
+		public event FileSystemEventHandler Deleted;
+
+		[Browsable(false)]
+		public event ErrorEventHandler Error;
+
+		[IODescription("Occurs when a file/directory rename matches the filter")]
+		public event RenamedEventHandler Renamed;
+
+		#endregion // Events and Delegates
+
+		/* 0 -> not supported	*/
+		/* 1 -> windows		*/
+		/* 2 -> FAM		*/
+		/* 3 -> Kevent		*/
+		/* 4 -> gamin		*/
+		/* 5 -> inotify		*/
+		//[MethodImplAttribute(MethodImplOptions.InternalCall)]
+		//static extern int InternalSupportsFSW ();
+	}
+}
+
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/IFileWatcher.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/IFileWatcher.cs
new file mode 100644
index 0000000..b761016
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/IFileWatcher.cs
@@ -0,0 +1,37 @@
+// 
+// System.IO.IFileWatcher.cs
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (c) 2004 Novell, Inc. (http://www.novell.com)
+//
+
+//
+// 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.LibraryWatcher.IO {
+	interface IFileWatcher {
+		void StartDispatching (FileSystemWatcher fsw);
+		void StopDispatching (FileSystemWatcher fsw);
+	}
+}
+
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/InotifyWatcher.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/InotifyWatcher.cs
new file mode 100644
index 0000000..d1af099
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/InotifyWatcher.cs
@@ -0,0 +1,624 @@
+// 
+// System.IO.Inotify.cs: interface with inotify
+//
+// Authors:
+//	Gonzalo Paniagua (gonzalo novell com)
+//	Anders Rune Jensen (anders iola dk)
+//
+// (c) 2006 Novell, Inc. (http://www.novell.com)
+
+//
+// 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.Collections;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+
+namespace Banshee.LibraryWatcher.IO {
+
+	[Flags]
+	enum InotifyMask : uint {
+		Access = 1 << 0,
+		Modify = 1 << 1,
+		Attrib = 1 << 2,
+		CloseWrite = 1 << 3,
+		CloseNoWrite = 1 << 4,
+		Open = 1 << 5,
+		MovedFrom = 1 << 6,
+		MovedTo = 1 << 7,
+		Create = 1 << 8,
+		Delete = 1 << 9,
+		DeleteSelf = 1 << 10,
+		MoveSelf = 1 << 11,
+		BaseEvents = 0x00000fff,
+		// Can be sent at any time
+		Umount = 0x0002000,
+		Overflow = 0x0004000,
+		Ignored = 0x0008000,
+
+		// Special flags.
+		OnlyDir = 0x01000000,
+		DontFollow = 0x02000000,
+		AddMask = 0x20000000,
+		Directory = 0x40000000,
+		OneShot = 0x80000000
+	}
+
+	struct InotifyEvent { // Our internal representation for the data returned by the kernel
+		public static readonly InotifyEvent Default = new InotifyEvent ();
+		public int WatchDescriptor;
+		public InotifyMask Mask;
+		public string Name;
+
+		public override string ToString ()
+		{
+			return String.Format ("[Descriptor: {0} Mask: {1} Name: {2}]", WatchDescriptor, Mask, Name);
+		}
+	}
+
+	class ParentInotifyData
+	{
+		public bool IncludeSubdirs;
+		public bool Enabled;
+	        public ArrayList children; // InotifyData
+	        public InotifyData data;
+	}
+
+	class InotifyData {
+		public FileSystemWatcher FSW;
+		public string Directory;
+		public int Watch;
+	}
+
+	class InotifyWatcher : IFileWatcher
+	{
+		static bool failed;
+		static InotifyWatcher instance;
+		static Hashtable watches; // FSW to ParentInotifyData
+		static Hashtable requests; // FSW to InotifyData
+		static IntPtr FD;
+		static Thread thread;
+		static bool stop;
+		
+		private InotifyWatcher ()
+		{
+		}
+		
+		// Locked by caller
+		public static bool GetInstance (out IFileWatcher watcher, bool gamin)
+		{
+			if (failed == true) {
+				watcher = null;
+				return false;
+			}
+
+			if (instance != null) {
+				watcher = instance;
+				return true;
+			}
+
+			FD = GetInotifyInstance ();
+			if ((long) FD == -1) {
+				failed = true;
+				watcher = null;
+				return false;
+			}
+
+			watches = Hashtable.Synchronized (new Hashtable ());
+			requests = Hashtable.Synchronized (new Hashtable ());
+			instance = new InotifyWatcher ();
+			watcher = instance;
+			return true;
+		}
+		
+		public void StartDispatching (FileSystemWatcher fsw)
+		{
+			ParentInotifyData parent;
+			lock (this) {
+				if ((long) FD == -1)
+					FD = GetInotifyInstance ();
+
+				if (thread == null) {
+					thread = new Thread (new ThreadStart (Monitor));
+					thread.IsBackground = true;
+					thread.Start ();
+				}
+
+				parent = (ParentInotifyData) watches [fsw];
+			}
+
+			if (parent == null) {
+				InotifyData data = new InotifyData ();
+				data.FSW = fsw;
+				data.Directory = fsw.FullPath;
+
+				parent = new ParentInotifyData();
+				parent.IncludeSubdirs = fsw.IncludeSubdirectories;
+				parent.Enabled = true;
+				parent.children = new ArrayList();
+				parent.data = data;
+
+				watches [fsw] = parent;
+
+				try {
+					StartMonitoringDirectory (data, false);
+					lock (this) {
+						AppendRequestData (data);
+						stop = false;
+					}
+				} catch {} // ignore the directory if StartMonitoringDirectory fails.
+			}
+		}
+		
+		static void AppendRequestData (InotifyData data)
+		{
+			int wd = data.Watch;
+
+			object obj = requests [wd];
+			ArrayList list = null;
+			if (obj == null) {
+				requests [data.Watch] = data;
+			} else if (obj is InotifyData) {
+				list = new ArrayList ();
+				list.Add (obj);
+				list.Add (data);
+				requests [data.Watch] = list;
+			} else {
+				list = (ArrayList) obj;
+				list.Add (data);
+			}
+		}
+
+		static bool RemoveRequestData (InotifyData data)
+		{
+			int wd = data.Watch;
+			object obj = requests [wd];
+			if (obj == null)
+				return true;
+
+			if (obj is InotifyData) {
+				if (obj == data) {
+					requests.Remove (wd);
+					return true;
+				}
+				return false;
+			}
+
+			ArrayList list = (ArrayList) obj;
+			list.Remove (data);
+			if (list.Count == 0) {
+				requests.Remove (wd);
+				return true;
+			}
+			return false;
+		}
+
+		// Attempt to match MS and linux behavior.
+		static InotifyMask GetMaskFromFilters (NotifyFilters filters)
+		{
+			InotifyMask mask = InotifyMask.Create | InotifyMask.Delete | InotifyMask.DeleteSelf | InotifyMask.AddMask;
+			if ((filters & NotifyFilters.Attributes) != 0)
+				mask |= InotifyMask.Attrib;
+
+			if ((filters & NotifyFilters.Security) != 0)
+				mask |= InotifyMask.Attrib;
+
+			if ((filters & NotifyFilters.Size) != 0) {
+				mask |= InotifyMask.Attrib;
+				mask |= InotifyMask.Modify;
+			}
+
+			if ((filters & NotifyFilters.LastAccess) != 0) {
+				mask |= InotifyMask.Attrib;
+				mask |= InotifyMask.Access;
+				mask |= InotifyMask.Modify;
+			}
+
+			if ((filters & NotifyFilters.LastWrite) != 0) {
+				mask |= InotifyMask.Attrib;
+				mask |= InotifyMask.Modify;
+			}
+
+			if ((filters & NotifyFilters.FileName) != 0) {
+				mask |= InotifyMask.MovedFrom;
+				mask |= InotifyMask.MovedTo;
+			}
+
+			if ((filters & NotifyFilters.DirectoryName) != 0) {
+				mask |= InotifyMask.MovedFrom;
+				mask |= InotifyMask.MovedTo;
+			}
+
+			return mask;
+		}
+
+		static void StartMonitoringDirectory (InotifyData data, bool justcreated)
+		{
+			InotifyMask mask = GetMaskFromFilters (data.FSW.NotifyFilter);
+			int wd = AddDirectoryWatch (FD, data.Directory, mask);
+			if (wd == -1) {
+				int error = Marshal.GetLastWin32Error ();
+				if (error == 4) { // Too many open watches
+					string nr_watches = "(unknown)";
+					try {
+						using (StreamReader reader = new StreamReader ("/proc/sys/fs/inotify/max_user_watches")) {
+							nr_watches = reader.ReadLine ();
+						}
+					} catch {}
+
+					string msg = String.Format ("The per-user inotify watches limit of {0} has been reached. " +
+								"If you're experiencing problems with your application, increase that limit " +
+								"in /proc/sys/fs/inotify/max_user_watches.", nr_watches);
+					
+					throw new Win32Exception (error, msg);
+				}
+				throw new Win32Exception (error);
+			}
+
+			FileSystemWatcher fsw = data.FSW;
+			data.Watch = wd;
+
+			ParentInotifyData parent = (ParentInotifyData) watches[fsw];
+
+			if (parent.IncludeSubdirs) {
+				foreach (string directory in Directory.GetDirectories (data.Directory)) {
+					InotifyData fd = new InotifyData ();
+					fd.FSW = fsw;
+					fd.Directory = directory;
+
+					if (justcreated) {
+						lock (fsw) {
+							RenamedEventArgs renamed = null;
+							if (fsw.Pattern.IsMatch (directory)) {
+								fsw.DispatchEvents (FileAction.Added, directory, ref renamed);
+								if (fsw.Waiting) {
+									fsw.Waiting = false;
+									System.Threading.Monitor.PulseAll (fsw);
+								}
+							}
+						}
+					}
+
+					try {
+						StartMonitoringDirectory (fd, justcreated);
+						AppendRequestData (fd);
+					        parent.children.Add(fd);
+					} catch {} // ignore errors and don't add directory.
+				}
+			}
+
+			if (justcreated) {
+				foreach (string filename in Directory.GetFiles (data.Directory)) {
+					lock (fsw) {
+						RenamedEventArgs renamed = null;
+						if (fsw.Pattern.IsMatch (filename)) {
+							fsw.DispatchEvents (FileAction.Added, filename, ref renamed);
+							/* If a file has been created, then it has been written to */
+							fsw.DispatchEvents (FileAction.Modified, filename, ref renamed);
+
+							if (fsw.Waiting) {
+								fsw.Waiting = false;
+								System.Threading.Monitor.PulseAll(fsw);
+							}
+						}
+					}
+				}
+			}
+		}
+
+		public void StopDispatching (FileSystemWatcher fsw)
+		{
+			ParentInotifyData parent;
+			lock (this) {
+				parent = (ParentInotifyData) watches [fsw];
+				if (parent == null)
+					return;
+
+				if (RemoveRequestData (parent.data)) {
+					StopMonitoringDirectory (parent.data);
+				}
+				watches.Remove (fsw);
+				if (watches.Count == 0) {
+					stop = true;
+					IntPtr fd = FD;
+					FD = (IntPtr) (-1);
+					Close (fd);
+				}
+
+				if (!parent.IncludeSubdirs)
+					return;
+
+				foreach (InotifyData idata in parent.children)
+				{
+				    if (RemoveRequestData (idata)) {
+					StopMonitoringDirectory (idata);
+				    }
+				}
+			}
+		}
+
+		static void StopMonitoringDirectory (InotifyData data)
+		{
+			RemoveWatch (FD, data.Watch);
+		}
+
+		void Monitor ()
+		{
+			byte [] buffer = new byte [4096];
+			int nread;
+			while (!stop) {
+				nread = ReadFromFD (FD, buffer, (IntPtr) buffer.Length);
+				if (nread == -1)
+					continue;
+
+				lock (this) {
+					ProcessEvents (buffer, nread);
+
+				}
+			}
+
+			lock (this) {
+				thread = null;
+				stop = false;
+			}
+		}
+		/*
+		struct inotify_event {
+			__s32           wd;
+			__u32           mask;
+			__u32           cookie;
+			__u32           len;		// Includes any trailing null in 'name'
+			char            name[0];
+		};
+		*/
+
+		static int ReadEvent (byte [] source, int off, int size, out InotifyEvent evt)
+		{
+			evt = new InotifyEvent ();
+			if (size <= 0 || off > size - 16) {
+				return -1;
+			}
+
+			int len;
+			if (BitConverter.IsLittleEndian) {
+				evt.WatchDescriptor = source [off] + (source [off + 1] << 8) +
+							(source [off + 2] << 16) + (source [off + 3] << 24);
+				evt.Mask = (InotifyMask) (source [off + 4] + (source [off + 5] << 8) +
+							(source [off + 6] << 16) + (source [off + 7] << 24));
+				// Ignore Cookie -> +4
+				len = source [off + 12] + (source [off + 13] << 8) +
+					(source [off + 14] << 16) + (source [off + 15] << 24);
+			} else {
+				evt.WatchDescriptor = source [off + 3] + (source [off + 2] << 8) +
+							(source [off + 1] << 16) + (source [off] << 24);
+				evt.Mask = (InotifyMask) (source [off + 7] + (source [off + 6] << 8) +
+							(source [off + 5] << 16) + (source [off + 4] << 24));
+				// Ignore Cookie -> +4
+				len = source [off + 15] + (source [off + 14] << 8) +
+					(source [off + 13] << 16) + (source [off + 12] << 24);
+			}
+
+			if (len > 0) {
+				if (off > size - 16 - len)
+					return -1;
+				string name = Encoding.UTF8.GetString (source, off + 16, len);
+				evt.Name = name.Trim ('\0');
+			} else {
+				evt.Name = null;
+			}
+
+			return 16 + len;
+		}
+
+		static IEnumerable GetEnumerator (object source)
+		{
+			if (source == null)
+				yield break;
+
+			if (source is InotifyData)
+				yield return source;
+
+			if (source is ArrayList) {
+				ArrayList list = (ArrayList) source;
+				for (int i = 0; i < list.Count; i++)
+					yield return list [i];
+			}
+		}
+
+		/* Interesting events:
+			* Modify
+			* Attrib
+			* MovedFrom
+			* MovedTo
+			* Create
+			* Delete
+			* DeleteSelf
+		*/
+		static InotifyMask Interesting = InotifyMask.Modify | InotifyMask.Attrib | InotifyMask.MovedFrom |
+							InotifyMask.MovedTo | InotifyMask.Create | InotifyMask.Delete |
+							InotifyMask.DeleteSelf;
+
+		void ProcessEvents (byte [] buffer, int length)
+		{
+			ArrayList newdirs = null;
+			InotifyEvent evt;
+			int nread = 0;
+			RenamedEventArgs renamed = null;
+			while (length > nread) {
+				int bytes_read = ReadEvent (buffer, nread, length, out evt);
+				if (bytes_read <= 0)
+					break;
+
+				nread += bytes_read;
+
+				InotifyMask mask = evt.Mask;
+				bool is_directory = (mask & InotifyMask.Directory) != 0;
+				mask = (mask & Interesting); // Clear out all the bits that we don't need
+				if (mask == 0)
+					continue;
+
+				foreach (InotifyData data in GetEnumerator (requests [evt.WatchDescriptor])) {
+				        ParentInotifyData parent = (ParentInotifyData) watches[data.FSW];
+
+					if (data == null || parent.Enabled == false)
+						continue;
+
+					string directory = data.Directory;
+					string filename = evt.Name;
+					if (filename == null)
+						filename = directory;
+
+					FileSystemWatcher fsw = data.FSW;
+					FileAction action = 0;
+					if ((mask & (InotifyMask.Modify | InotifyMask.Attrib)) != 0) {
+						action = FileAction.Modified;
+					} else if ((mask & InotifyMask.Create) != 0) {
+						action = FileAction.Added;
+					} else if ((mask & InotifyMask.Delete) != 0) {
+						action = FileAction.Removed;
+					} else if ((mask & InotifyMask.DeleteSelf) != 0) {
+						if (data.Watch != parent.data.Watch) {
+							// To avoid duplicate events handle DeleteSelf only for the top level directory.
+							continue;
+						}
+						action = FileAction.Removed;
+					} else if ((mask & InotifyMask.MoveSelf) != 0) {
+						//action = FileAction.Removed;
+						continue; // Ignore this one
+					} else if ((mask & InotifyMask.MovedFrom) != 0) {
+						InotifyEvent to;
+						int i = ReadEvent (buffer, nread, length, out to);
+						if (i == -1 || (to.Mask & InotifyMask.MovedTo) == 0 || evt.WatchDescriptor != to.WatchDescriptor) {
+							action = FileAction.Removed;
+						} else {
+							nread += i;
+							action = FileAction.RenamedNewName;
+							renamed = new RenamedEventArgs (WatcherChangeTypes.Renamed, data.Directory, to.Name, evt.Name);
+							if (evt.Name != data.Directory && !fsw.Pattern.IsMatch (evt.Name))
+								filename = to.Name;
+						}
+					} else if ((mask & InotifyMask.MovedTo) != 0) {
+						action = FileAction.Added;
+					}
+					if (fsw.IncludeSubdirectories) {
+						string full = fsw.FullPath;
+						string datadir = data.Directory;
+						if (datadir != full) {
+							int len = full.Length;
+							int slash = 1;
+							if (len > 1 && full [len - 1] == Path.DirectorySeparatorChar)
+								slash = 0;
+							string reldir = datadir.Substring (full.Length + slash);
+							datadir = Path.Combine (datadir, filename);
+							filename = Path.Combine (reldir, filename);
+						} else {
+							datadir = Path.Combine (full, filename);
+						}
+
+						if (action == FileAction.Added && is_directory) {
+							if (newdirs == null)
+								newdirs = new ArrayList (2);
+
+							InotifyData fd = new InotifyData ();
+							fd.FSW = fsw;
+							fd.Directory = datadir;
+							newdirs.Add (fd);
+						}
+
+						if (action == FileAction.RenamedNewName && is_directory) {
+							foreach (InotifyData child in parent.children) {
+								if (child.Directory.StartsWith (renamed.OldFullPath)) {
+									child.Directory = renamed.FullPath +
+										child.Directory.Substring (renamed.OldFullPath.Length);
+								}
+							}
+						}
+					}
+
+					if (action == FileAction.Removed && filename == data.Directory) {
+						int idx = parent.children.IndexOf (data);
+						if (idx != -1) {
+							parent.children.RemoveAt (idx);
+							if (!fsw.Pattern.IsMatch (Path.GetFileName (filename))) {
+								continue;
+							}
+						}
+					}
+
+					if (filename != data.Directory && !fsw.Pattern.IsMatch (Path.GetFileName (filename))) {
+						continue;
+					}
+
+					lock (fsw) {
+						fsw.DispatchEvents (action, filename, ref renamed);
+						if (action == FileAction.RenamedNewName)
+							renamed = null;
+						if (fsw.Waiting) {
+							fsw.Waiting = false;
+							System.Threading.Monitor.PulseAll (fsw);
+						}
+					}
+				}
+			}
+
+			if (newdirs != null) {
+			        foreach (InotifyData newdir in newdirs) {
+					try {
+						StartMonitoringDirectory (newdir, true);
+						AppendRequestData (newdir);
+					        ((ParentInotifyData) watches[newdir.FSW]).children.Add(newdir);
+					} catch {} // ignore the given directory
+				}
+				newdirs.Clear ();
+			}
+		}
+
+		static int AddDirectoryWatch (IntPtr fd, string directory, InotifyMask mask)
+		{
+			mask |= InotifyMask.Directory;
+			return AddWatch (fd, directory, mask);
+		}
+
+		[DllImport ("libc", EntryPoint="close")]
+		internal extern static int Close (IntPtr fd);
+
+		[DllImport ("libc", EntryPoint = "read")]
+		extern static int ReadFromFD (IntPtr fd, byte [] buffer, IntPtr length);
+
+		//[MethodImplAttribute(MethodImplOptions.InternalCall)]
+        [DllImport ("libc", EntryPoint = "inotify_init")]
+		extern static IntPtr GetInotifyInstance ();
+
+		//[MethodImplAttribute(MethodImplOptions.InternalCall)]
+        [DllImport ("libc", EntryPoint = "inotify_add_watch")]
+		extern static int AddWatch (IntPtr fd, string name, InotifyMask mask);
+
+		//[MethodImplAttribute(MethodImplOptions.InternalCall)]
+        [DllImport ("libc", EntryPoint = "inotify_rm_watch")]
+		extern static IntPtr RemoveWatch (IntPtr fd, int wd);
+	}
+}
+
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/SearchPattern.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/SearchPattern.cs
new file mode 100644
index 0000000..cb3adf4
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/IO/SearchPattern.cs
@@ -0,0 +1,225 @@
+//
+// System.IO.SearchPattern2.cs: Filename glob support.
+//
+// Author:
+//   Dan Lewis (dihlewis yahoo co uk)
+//
+// (C) 2002
+//
+
+//
+// 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.
+//
+
+// Copied from corlib/System.IO/SearchPatter.cs
+using System;
+using System.IO;
+
+namespace Banshee.LibraryWatcher.IO {
+
+	// FIXME: there's a complication with this algorithm under windows.
+	// the pattern '*.*' matches all files (i think . matches the extension),
+	// whereas under UNIX it should only match files containing the '.' character.
+
+	class SearchPattern2 {
+		public SearchPattern2 (string pattern) : this (pattern, false) { }
+
+		public SearchPattern2 (string pattern, bool ignore)
+		{
+			this.ignore = ignore;
+			this.pattern = pattern;
+			Compile (pattern);
+		}
+
+		// OSX has a retarded case-insensitive yet case-aware filesystem
+		// so we need a overload in here for the Kqueue watcher
+		public bool IsMatch (string text, bool ignorecase)
+		{
+			if (!hasWildcard) {
+				bool match = String.Compare (pattern, text, ignorecase) == 0;
+				if (match)
+					return true;
+				
+				// This is a special case for FSW. It needs to match e.g. subdir/file.txt
+				// when the pattern is "file.txt"
+				int idx = text.LastIndexOf ('/');
+				if (idx == -1)
+					return false;
+				idx++;
+				if (idx == text.Length)
+					return false;
+				
+				return (String.Compare (pattern, text.Substring (idx), ignorecase) == 0);
+			}
+			
+			return Match (ops, text, 0);
+		}
+
+		public bool IsMatch (string text)
+		{
+			return IsMatch (text, ignore);
+		}
+
+		public bool HasWildcard {
+			get { return hasWildcard; }
+		}
+		// private
+
+		Op ops;		// the compiled pattern
+		bool ignore;	// ignore case
+		bool hasWildcard;
+		string pattern;
+
+		private void Compile (string pattern)
+		{
+			if (pattern == null || pattern.IndexOfAny (InvalidChars) >= 0)
+				throw new ArgumentException ("Invalid search pattern: '" + pattern + "'");
+
+			if (pattern == "*") {	// common case
+				ops = new Op (OpCode.True);
+				hasWildcard = true;
+				return;
+			}
+
+			ops = null;
+
+			int ptr = 0;
+			Op last_op = null;
+			while (ptr < pattern.Length) {
+				Op op;
+			
+				switch (pattern [ptr]) {
+				case '?':
+					op = new Op (OpCode.AnyChar);
+					++ ptr;
+					hasWildcard = true;
+					break;
+
+				case '*':
+					op = new Op (OpCode.AnyString);
+					++ ptr;
+					hasWildcard = true;
+					break;
+					
+				default:
+					op = new Op (OpCode.ExactString);
+					int end = pattern.IndexOfAny (WildcardChars, ptr);
+					if (end < 0)
+						end = pattern.Length;
+
+					op.Argument = pattern.Substring (ptr, end - ptr);
+					if (ignore)
+						op.Argument = op.Argument.ToLower ();
+
+					ptr = end;
+					break;
+				}
+
+				if (last_op == null)
+					ops = op;
+				else
+					last_op.Next = op;
+
+				last_op = op;
+			}
+
+			if (last_op == null)
+				ops = new Op (OpCode.End);
+			else
+				last_op.Next = new Op (OpCode.End);
+		}
+
+		private bool Match (Op op, string text, int ptr)
+		{
+			while (op != null) {
+				switch (op.Code) {
+				case OpCode.True:
+					return true;
+
+				case OpCode.End:
+					if (ptr == text.Length)
+						return true;
+
+					return false;
+				
+				case OpCode.ExactString:
+					int length = op.Argument.Length;
+					if (ptr + length > text.Length)
+						return false;
+
+					string str = text.Substring (ptr, length);
+					if (ignore)
+						str = str.ToLower ();
+
+					if (str != op.Argument)
+						return false;
+
+					ptr += length;
+					break;
+
+				case OpCode.AnyChar:
+					if (++ ptr > text.Length)
+						return false;
+					break;
+
+				case OpCode.AnyString:
+					while (ptr <= text.Length) {
+						if (Match (op.Next, text, ptr))
+							return true;
+
+						++ ptr;
+					}
+
+					return false;
+				}
+
+				op = op.Next;
+			}
+
+			return true;
+		}
+
+		// private static
+
+		internal static readonly char [] WildcardChars = { '*', '?' };
+		internal static readonly char [] InvalidChars = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
+
+		private class Op {
+			public Op (OpCode code)
+			{
+				this.Code = code;
+				this.Argument = null;
+				this.Next = null;
+			}
+		
+			public OpCode Code;
+			public string Argument;
+			public Op Next;
+		}
+
+		private enum OpCode {
+			ExactString,		// literal
+			AnyChar,		// ?
+			AnyString,		// *
+			End,			// end of pattern
+			True			// always succeeds
+		};
+	}
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/LibraryWatcherService.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/LibraryWatcherService.cs
new file mode 100644
index 0000000..fee1aed
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/LibraryWatcherService.cs
@@ -0,0 +1,120 @@
+//
+// LibraryWatcherService.cs
+//
+// Authors:
+//   Alexander Hixon <hixon alexander mediati org>
+//   Christian Martellini <christian martellini gmail com>
+//
+// Copyright (C) 2008 Alexander Hixon
+// Copyright (C) 2009 Christian Martellini
+//
+// 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.Base ;
+using Banshee.Sources;
+using Banshee.ServiceStack;
+using Banshee.Library;
+
+namespace Banshee.LibraryWatcher
+{
+    public class LibraryWatcherService : IExtensionService, IDisposable, IDelayedInitializeService
+    {
+        private readonly Dictionary<LibrarySource, SourceWatcher> watchers =
+            new Dictionary<LibrarySource, SourceWatcher> ();
+
+        string IService.ServiceName {
+            get { return "LibraryWatcherService"; }
+        }
+
+        void IExtensionService.Initialize ()
+        {
+        }
+
+        public void DelayedInitialize ()
+        {
+            ServiceManager.SourceManager.SourceAdded += OnSourceAdded;
+            ServiceManager.SourceManager.SourceRemoved += OnSourceRemoved;
+
+            foreach (var library in ServiceManager.SourceManager.FindSources<LibrarySource> ()) {
+                AddLibrary (library);
+            }
+        }
+
+        public void Dispose ()
+        {
+            lock (watchers) {
+                foreach (var watcher in watchers.Values) {
+                    watcher.Dispose ();
+                }
+                watchers.Clear ();
+            }
+        }
+
+        private void OnSourceAdded (SourceAddedArgs args)
+        {
+            var library = args.Source as LibrarySource;
+            if (library != null) {
+                AddLibrary (library);
+            }
+        }
+
+        private void OnSourceRemoved (SourceEventArgs args)
+        {
+            var library = args.Source as LibrarySource;
+            if (library != null) {
+                RemoveLibrary (library);
+            }
+        }
+
+        private void AddLibrary (LibrarySource library)
+        {
+            if (!Banshee.IO.Directory.Exists(library.BaseDirectoryWithSeparator)) {
+                Hyena.Log.DebugFormat ("Will not watch library {0} because its folder doesn't exist: {1}",
+                    library.Name, library.BaseDirectoryWithSeparator);
+                return;
+            }
+            lock (watchers) {
+                if (!watchers.ContainsKey (library)) {
+                    try {
+                        watchers[library] = new SourceWatcher (library);
+                        Hyena.Log.DebugFormat ("Started LibraryWatcher for {0} ({1})",
+                            library.Name, library.BaseDirectoryWithSeparator);
+                    } catch (Exception e) {
+                        Hyena.Log.Exception (e);
+                    }
+                }
+            }
+        }
+
+        private void RemoveLibrary (LibrarySource library)
+        {
+            lock (watchers) {
+                if (watchers.ContainsKey (library)) {
+                    watchers[library].Dispose ();
+                    watchers.Remove (library);
+                }
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/SourceWatcher.cs b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/SourceWatcher.cs
new file mode 100644
index 0000000..b2d1b35
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Banshee.LibraryWatcher/SourceWatcher.cs
@@ -0,0 +1,293 @@
+//
+// SourceWatcher.cs
+//
+// Authors:
+//   Christian Martellini <christian martellini gmail com>
+//   Alexander Kojevnikov <alexander kojevnikov com>
+//
+// Copyright (C) 2009 Christian Martellini
+// Copyright (C) 2009 Alexander Kojevnikov
+//
+// 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.Linq;
+using System.Data;
+using System.Threading;
+using System.Collections.Generic;
+
+using Hyena;
+using Hyena.Data.Sqlite;
+
+using Banshee.Base;
+using Banshee.Collection;
+using Banshee.Collection.Database;
+using Banshee.Library;
+using Banshee.ServiceStack;
+using Banshee.Sources;
+using Banshee.Streaming;
+
+namespace Banshee.LibraryWatcher
+{
+    public class SourceWatcher : IDisposable
+    {
+        private readonly LibraryImportManager import_manager;
+        private readonly LibrarySource library;
+        private readonly FileSystemWatcherProxy watcher;
+        private readonly ManualResetEvent handle;
+        private readonly Thread watch_thread;
+
+        private readonly Queue<QueueItem> queue = new Queue<QueueItem> ();
+        private readonly TimeSpan delay = TimeSpan.FromMilliseconds (1000);
+
+        private bool active;
+        private bool disposed;
+
+        private class QueueItem
+        {
+            public DateTime When;
+            public WatcherChangeTypes ChangeType;
+            public string OldFullPath;
+            public string FullPath;
+            public string MetadataHash;
+        }
+
+        public SourceWatcher (LibrarySource library)
+        {
+            this.library = library;
+            handle = new ManualResetEvent(false);
+            string path = library.BaseDirectoryWithSeparator;
+            string home = Environment.GetFolderPath (Environment.SpecialFolder.Personal) + Path.DirectorySeparatorChar;
+            if (path == home) {
+                throw new Exception ("Will not create LibraryWatcher for the entire home directory");
+            }
+
+            import_manager = ServiceManager.Get<LibraryImportManager> ();
+
+            watcher = new FileSystemWatcherProxy (path);
+            watcher.IncludeSubdirectories = true;
+            watcher.Changed += OnChanged;
+            watcher.Created += OnChanged;
+            watcher.Deleted += OnChanged;
+            watcher.Renamed += OnChanged;
+
+            active = true;
+            watch_thread = new Thread (new ThreadStart (Watch));
+            watch_thread.Name = String.Format ("LibraryWatcher for {0}", library.Name);
+            watch_thread.IsBackground = true;
+            watch_thread.Start ();
+        }
+
+#region Public Methods
+
+        public void Dispose ()
+        {
+            if (!disposed) {
+                watcher.EnableRaisingEvents = false;
+                active = false;
+                watcher.Changed -= OnChanged;
+                watcher.Created -= OnChanged;
+                watcher.Deleted -= OnChanged;
+                watcher.Renamed -= OnChanged;
+
+                watcher.Dispose ();
+                disposed = true;
+            }
+        }
+
+#endregion
+
+#region Private Methods
+
+        private void OnChanged (object source, FileSystemEventArgs args)
+        {
+            var item = new QueueItem {
+                When = DateTime.Now,
+                ChangeType = args.ChangeType,
+                FullPath = args.FullPath,
+                OldFullPath = args is RenamedEventArgs ? ((RenamedEventArgs)args).OldFullPath : args.FullPath
+            };
+
+            lock (queue) {
+                queue.Enqueue (item);
+            }
+            handle.Set ();
+
+            if (args.ChangeType != WatcherChangeTypes.Changed) {
+                Hyena.Log.DebugFormat ("Watcher: {0} {1}{2}",
+                    item.ChangeType, args is RenamedEventArgs ? item.OldFullPath + " => " : "", item.FullPath);
+            }
+        }
+
+        private void Watch ()
+        {
+            watcher.EnableRaisingEvents = true;
+
+            while (active) {
+                WatcherChangeTypes change_types = 0;
+                while (queue.Count > 0) {
+                    QueueItem item;
+                    lock (queue) {
+                        item = queue.Dequeue ();
+                    }
+
+                    int sleep =  (int) (item.When + delay - DateTime.Now).TotalMilliseconds;
+                    if (sleep > 0) {
+                        Hyena.Log.DebugFormat ("Watcher: sleeping {0}ms", sleep);
+                        Thread.Sleep (sleep);
+                    }
+
+                    if (item.ChangeType == WatcherChangeTypes.Changed) {
+                        UpdateTrack (item.FullPath);
+                    } else if (item.ChangeType == WatcherChangeTypes.Created) {
+                        AddTrack (item.FullPath);
+                    } else if (item.ChangeType == WatcherChangeTypes.Deleted) {
+                        RemoveTrack (item.FullPath);
+                    } else if (item.ChangeType == WatcherChangeTypes.Renamed) {
+                        RenameTrack (item.OldFullPath, item.FullPath);
+                    }
+
+                    change_types |= item.ChangeType;
+                }
+
+                if ((change_types & WatcherChangeTypes.Deleted) > 0) {
+                    library.NotifyTracksDeleted ();
+                }
+                if ((change_types & (WatcherChangeTypes.Renamed |
+                    WatcherChangeTypes.Created | WatcherChangeTypes.Changed)) > 0) {
+                    library.NotifyTracksChanged ();
+                }
+
+                handle.WaitOne ();
+                handle.Reset ();
+            }
+        }
+
+        private void UpdateTrack (string track)
+        {
+            using (var reader = ServiceManager.DbConnection.Query (
+                DatabaseTrackInfo.Provider.CreateFetchCommand (
+                "CoreTracks.Uri = ? LIMIT 1"), new SafeUri (track).AbsoluteUri)) {
+                if (reader.Read ()) {
+                    var track_info = DatabaseTrackInfo.Provider.Load (reader);
+                    if (Banshee.IO.File.GetModifiedTime (track_info.Uri) > track_info.FileModifiedStamp) {
+                        var file = StreamTagger.ProcessUri (track_info.Uri);
+                        StreamTagger.TrackInfoMerge (track_info, file, false);
+                        track_info.LastSyncedStamp = DateTime.Now;
+                        track_info.Save (false);
+                    }
+                }
+            }
+        }
+
+        private void AddTrack (string track)
+        {
+            import_manager.ImportTrack (track);
+
+            // Trigger file rename.
+            string uri = new SafeUri(track).AbsoluteUri;
+            HyenaSqliteCommand command = new HyenaSqliteCommand (@"
+                UPDATE CoreTracks
+                SET DateUpdatedStamp = LastSyncedStamp + 1
+                WHERE Uri = ?", uri);
+            ServiceManager.DbConnection.Execute (command);
+        }
+
+        private void RemoveTrack (string track)
+        {
+            string uri = new SafeUri(track).AbsoluteUri;
+            const string hash_sql = @"SELECT TrackID, MetadataHash FROM CoreTracks WHERE Uri = ? LIMIT 1";
+            int track_id = 0;
+            string hash = null;
+            using (var reader = new HyenaDataReader (ServiceManager.DbConnection.Query (hash_sql, uri))) {
+                if (reader.Read ()) {
+                    track_id = reader.Get<int> (0);
+                    hash = reader.Get<string> (1);
+                }
+            }
+
+            if (hash != null) {
+                QueueItem item;
+                lock (queue) {
+                    item = queue.FirstOrDefault (
+                        i => i.ChangeType == WatcherChangeTypes.Created && GetMetadataHash(i) == hash);
+                }
+                if (item != null) {
+                    item.ChangeType = WatcherChangeTypes.Renamed;
+                    item.OldFullPath = track;
+                    return;
+                }
+            }
+
+            const string delete_sql = @"
+                INSERT INTO CoreRemovedTracks (DateRemovedStamp, TrackID, Uri)
+                SELECT ?, TrackID, Uri FROM CoreTracks WHERE TrackID IN ({0})
+                ;
+                DELETE FROM CoreTracks WHERE TrackID IN ({0})";
+
+            // If track_id is 0, it's a directory.
+            HyenaSqliteCommand delete_command;
+            if (track_id > 0) {
+                delete_command = new HyenaSqliteCommand (String.Format (delete_sql,
+                    "?"), DateTime.Now, track_id, track_id);
+            } else {
+                string pattern = StringUtil.EscapeLike (uri) + "/_%";
+                delete_command = new HyenaSqliteCommand (String.Format (delete_sql,
+                    @"SELECT TrackID FROM CoreTracks WHERE Uri LIKE ? ESCAPE '\'"), DateTime.Now, pattern, pattern);
+            }
+
+            ServiceManager.DbConnection.Execute (delete_command);
+        }
+
+        private void RenameTrack(string oldFullPath, string fullPath)
+        {
+            if (oldFullPath == fullPath) {
+                // FIXME: bug in Mono, see bnc#322330
+                return;
+            }
+            string old_uri = new SafeUri (oldFullPath).AbsoluteUri;
+            string new_uri = new SafeUri (fullPath).AbsoluteUri;
+            string pattern = StringUtil.EscapeLike (old_uri) + "%";
+            HyenaSqliteCommand rename_command = new HyenaSqliteCommand (@"
+                UPDATE CoreTracks
+                SET Uri = REPLACE(Uri, ?, ?), DateUpdatedStamp = ?
+                WHERE Uri LIKE ? ESCAPE '\'",
+                old_uri, new_uri, DateTime.Now, pattern);
+            ServiceManager.DbConnection.Execute (rename_command);
+        }
+
+        private string GetMetadataHash (QueueItem item)
+        {
+            if (item.ChangeType == WatcherChangeTypes.Created && item.MetadataHash == null) {
+                var uri = new SafeUri (item.FullPath);
+                if (DatabaseImportManager.IsWhiteListedFile (item.FullPath) && Banshee.IO.File.Exists (uri)) {
+                    var track = new TrackInfo ();
+                    StreamTagger.TrackInfoMerge (track, StreamTagger.ProcessUri (uri));
+                    item.MetadataHash = track.MetadataHash;
+                }
+            }
+            return item.MetadataHash;
+        }
+
+#endregion
+    }
+}
diff --git a/src/Extensions/Banshee.LibraryWatcher/Makefile.am b/src/Extensions/Banshee.LibraryWatcher/Makefile.am
new file mode 100644
index 0000000..64fbd62
--- /dev/null
+++ b/src/Extensions/Banshee.LibraryWatcher/Makefile.am
@@ -0,0 +1,19 @@
+ASSEMBLY = Banshee.LibraryWatcher
+TARGET = library
+LINK = $(REF_EXTENSION_LIBRARYWATCHER)
+INSTALL_DIR = $(EXTENSIONS_INSTALL_DIR)
+
+SOURCES =  \
+	Banshee.LibraryWatcher/FileSystemWatcherProxy.cs \
+	Banshee.LibraryWatcher/IO/FileAction.cs \
+	Banshee.LibraryWatcher/IO/FileSystemWatcher.cs \
+	Banshee.LibraryWatcher/IO/IFileWatcher.cs \
+	Banshee.LibraryWatcher/IO/InotifyWatcher.cs \
+	Banshee.LibraryWatcher/IO/SearchPattern.cs \
+	Banshee.LibraryWatcher/LibraryWatcherService.cs \
+	Banshee.LibraryWatcher/SourceWatcher.cs
+
+RESOURCES = Banshee.LibraryWatcher.addin.xml
+
+include $(top_srcdir)/build/build.mk
+
diff --git a/src/Extensions/Makefile.am b/src/Extensions/Makefile.am
index c38928e..eae043e 100644
--- a/src/Extensions/Makefile.am
+++ b/src/Extensions/Makefile.am
@@ -9,6 +9,7 @@ SUBDIRS = \
 	Banshee.InternetArchive \
 	Banshee.InternetRadio \
 	Banshee.Lastfm \
+	Banshee.LibraryWatcher \
 	Banshee.MiniMode \
 	Banshee.MultimediaKeys \
 	Banshee.NotificationArea \
@@ -23,4 +24,5 @@ SUBDIRS = \
 	Banshee.RemoteAudio \
 	Banshee.Wikipedia
 
+
 MAINTAINERCLEANFILES = Makefile.in
diff --git a/src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs b/src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs
index a707283..2572e91 100644
--- a/src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs
+++ b/src/Libraries/Hyena/Hyena.Query/StringQueryValue.cs
@@ -90,9 +90,7 @@ namespace Hyena.Query
 
             if (op == Contains   || op == DoesNotContain ||
                 op == StartsWith || op == EndsWith) {
-                return orig.Replace ("\\", "\\\\")
-                           .Replace ("%", "\\%")
-                           .Replace ("_", "\\_");
+                return StringUtil.EscapeLike (orig);
             }
 
             return orig;
diff --git a/src/Libraries/Hyena/Hyena/StringUtil.cs b/src/Libraries/Hyena/Hyena/StringUtil.cs
index 9070b5c..dbf03ac 100644
--- a/src/Libraries/Hyena/Hyena/StringUtil.cs
+++ b/src/Libraries/Hyena/Hyena/StringUtil.cs
@@ -328,5 +328,14 @@ namespace Hyena
                 position = index + 1;
             }
         }
+
+        private static readonly char[] escaped_like_chars = new char[] {'\\', '%', '_'};
+        public static string EscapeLike (string s)
+        {
+            if (s.IndexOfAny (escaped_like_chars) != -1) {
+                return s.Replace (@"\", @"\\").Replace ("%", @"\%").Replace ("_", @"\_");
+            }
+            return s;
+        }
     }
 }



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