[banshee] [Fixup] New extension for bulk metadata fixing



commit fcfb809ffa8bdbef7a8cab532c056abd218c5e43
Author: Gabriel Burt <gabriel burt gmail com>
Date:   Thu Jul 15 20:16:41 2010 -0700

    [Fixup] New extension for bulk metadata fixing
    
    Adds a 'Fix Music Metadata' action to the Tools menu, which when clicked
    opens a source below the Music library.  Currently can merge artists and
    albums that vary only by case, '&' vs 'and', etc.

 Banshee.sln                                        |    7 +
 build/build.environment.mk                         |    1 +
 configure.ac                                       |    1 +
 data/addin-xml-strings.cs                          |    5 +
 po/POTFILES.in                                     |    7 +-
 .../Banshee.Fixup/Banshee.Fixup.addin.xml          |   37 ++++
 src/Extensions/Banshee.Fixup/Banshee.Fixup.csproj  |  106 ++++++++++
 .../Banshee.Fixup/AlbumDuplicateSolver.cs          |  105 ++++++++++
 .../Banshee.Fixup/ArtistDuplicateSolver.cs         |  109 ++++++++++
 .../Banshee.Fixup/ColumnCellSolutionOptions.cs     |  129 ++++++++++++
 .../Banshee.Fixup/Banshee.Fixup/FixActions.cs      |   72 +++++++
 .../Banshee.Fixup/Banshee.Fixup/FixSource.cs       |  104 ++++++++++
 .../Banshee.Fixup/Banshee.Fixup/Problem.cs         |  136 ++++++++++++
 .../Banshee.Fixup/Banshee.Fixup/ProblemModel.cs    |  186 +++++++++++++++++
 .../Banshee.Fixup/Banshee.Fixup/Solver.cs          |  218 ++++++++++++++++++++
 src/Extensions/Banshee.Fixup/Banshee.Fixup/View.cs |  133 ++++++++++++
 src/Extensions/Banshee.Fixup/Makefile.am           |   23 ++
 .../Banshee.Fixup/Resources/ActiveUI.xml           |   13 ++
 .../Banshee.Fixup/Resources/GlobalUI.xml           |    7 +
 src/Extensions/Makefile.am                         |    1 +
 src/Hyena                                          |    2 +-
 21 files changed, 1400 insertions(+), 2 deletions(-)
---
diff --git a/Banshee.sln b/Banshee.sln
index a337da2..92ea5da 100644
--- a/Banshee.sln
+++ b/Banshee.sln
@@ -116,6 +116,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.FileSystemQueue", "
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.InternetRadio", "src\Extensions\Banshee.InternetRadio\Banshee.InternetRadio.csproj", "{12984BDF-C565-4452-AD47-79BD3C440E28}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.Fixup", "src\Extensions\Banshee.Fixup\Banshee.Fixup.csproj", "{9A8BCA22-82D6-4106-961A-DB4F77937BD5}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.SqlDebugConsole", "src\Extensions\Banshee.SqlDebugConsole\Banshee.SqlDebugConsole.csproj", "{0E1A7F20-E49B-4F9D-AEA0-2B1AD64326AC}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banshee.Bpm", "src\Extensions\Banshee.Bpm\Banshee.Bpm.csproj", "{471E44F3-B404-413B-8E95-BCCA70C6E834}"
@@ -203,6 +205,10 @@ Global
 		{12984BDF-C565-4452-AD47-79BD3C440E28}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{12984BDF-C565-4452-AD47-79BD3C440E28}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
 		{12984BDF-C565-4452-AD47-79BD3C440E28}.Windows|Any CPU.Build.0 = Windows|Any CPU
+		{9A8BCA22-82D6-4106-961A-DB4F77937BD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9A8BCA22-82D6-4106-961A-DB4F77937BD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9A8BCA22-82D6-4106-961A-DB4F77937BD5}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
+		{9A8BCA22-82D6-4106-961A-DB4F77937BD5}.Windows|Any CPU.Build.0 = Windows|Any CPU
 		{16FB0D3A-53FA-4B8E-B02B-4AF66E87829A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{16FB0D3A-53FA-4B8E-B02B-4AF66E87829A}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{16FB0D3A-53FA-4B8E-B02B-4AF66E87829A}.Windows|Any CPU.ActiveCfg = Windows|Any CPU
@@ -482,6 +488,7 @@ Global
 		{0092BF81-ECAB-4D0C-8691-6D19FB7E04A1} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{A993A473-1A18-4D12-ADF1-9CF3E0E12637} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{12984BDF-C565-4452-AD47-79BD3C440E28} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
+		{9A8BCA22-82D6-4106-961A-DB4F77937BD5} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{0E1A7F20-E49B-4F9D-AEA0-2B1AD64326AC} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{471E44F3-B404-413B-8E95-BCCA70C6E834} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
 		{9A5328D7-B7FB-4966-BF03-A4BA541541F5} = {4DD1DE63-F20B-4FC3-8FDA-F0BDF4183722}
diff --git a/build/build.environment.mk b/build/build.environment.mk
index 82c1082..76f6257 100644
--- a/build/build.environment.mk
+++ b/build/build.environment.mk
@@ -137,6 +137,7 @@ REF_EXTENSION_MINIMODE = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_MEEGO = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 LINK_EXTENSION_MEEGO = -r:$(DIR_BIN)/Banshee.MeeGo.dll $(REF_EXTENSION_MEEGO)
 REF_EXTENSION_MULTIMEDIAKEYS = $(LINK_BANSHEE_SERVICES_DEPS)
+REF_EXTENSION_FIXUP = $(LINK_BANSHEE_THICKCLIENT_DEPS) $(LINK_MUSICBRAINZ_DEPS)
 REF_EXTENSION_NOTIFICATIONAREA = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_PLAYER_MIGRATION = $(LINK_BANSHEE_THICKCLIENT_DEPS)
 REF_EXTENSION_PLAYQUEUE = $(LINK_BANSHEE_THICKCLIENT_DEPS)
diff --git a/configure.ac b/configure.ac
index 542e74c..6830799 100644
--- a/configure.ac
+++ b/configure.ac
@@ -340,6 +340,7 @@ src/Extensions/Banshee.Daap/Makefile
 src/Extensions/Banshee.Emusic/Makefile
 src/Extensions/Banshee.FileSystemQueue/Makefile
 src/Extensions/Banshee.InternetArchive/Makefile
+src/Extensions/Banshee.Fixup/Makefile
 src/Extensions/Banshee.InternetRadio/Makefile
 src/Extensions/Banshee.Lastfm/Makefile
 src/Extensions/Banshee.LastfmStreaming/Makefile
diff --git a/data/addin-xml-strings.cs b/data/addin-xml-strings.cs
index eb72ad0..22a9cc2 100644
--- a/data/addin-xml-strings.cs
+++ b/data/addin-xml-strings.cs
@@ -111,6 +111,11 @@ internal static class AddinXmlStringCatalog
         Catalog.GetString (@"Preview files without importing to your library.");
         Catalog.GetString (@"Core");
 
+        // ../src/Extensions/Banshee.Fixup/Banshee.Fixup.addin.xml
+        Catalog.GetString (@"Metadata Fixup");
+        Catalog.GetString (@"Fix up metadata using bulk operations");
+        Catalog.GetString (@"User Interface");
+
         // ../src/Extensions/Banshee.InternetArchive/Banshee.InternetArchive.addin.xml
         Catalog.GetString (@"Internet Archive");
         Catalog.GetString (@"Browse and search the Internet Archive's vast media collection.");
diff --git a/po/POTFILES.in b/po/POTFILES.in
index b230003..f61bfd4 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -211,6 +211,11 @@ src/Extensions/Banshee.Emusic/Banshee.Emusic/DownloadManager/DownloadUserJob.cs
 src/Extensions/Banshee.Emusic/Banshee.Emusic/EmusicImport.cs
 src/Extensions/Banshee.FileSystemQueue/Banshee.FileSystemQueue.addin.xml
 src/Extensions/Banshee.FileSystemQueue/Banshee.FileSystemQueue/FileSystemQueueSource.cs
+src/Extensions/Banshee.Fixup/Banshee.Fixup/AlbumDuplicateSolver.cs
+src/Extensions/Banshee.Fixup/Banshee.Fixup/ArtistDuplicateSolver.cs
+src/Extensions/Banshee.Fixup/Banshee.Fixup/FixActions.cs
+src/Extensions/Banshee.Fixup/Banshee.Fixup/FixSource.cs
+src/Extensions/Banshee.Fixup/Banshee.Fixup/View.cs
 src/Extensions/Banshee.InternetArchive/Banshee.InternetArchive/Actions.cs
 src/Extensions/Banshee.InternetArchive/Banshee.InternetArchive.addin.xml
 src/Extensions/Banshee.InternetArchive/Banshee.InternetArchive/DetailsSource.cs
@@ -249,6 +254,7 @@ src/Extensions/Banshee.MiniMode/Banshee.MiniMode.addin.xml
 src/Extensions/Banshee.MiniMode/Banshee.MiniMode/MiniModeService.cs
 src/Extensions/Banshee.MiniMode/Banshee.MiniMode/MiniModeWindow.cs
 src/Extensions/Banshee.MiroGuide/Banshee.MiroGuide/MiroGuideSource.cs
+src/Extensions/Banshee.MiroGuide/Banshee.MiroGuide/MiroGuideView.cs
 src/Extensions/Banshee.MultimediaKeys/Banshee.MultimediaKeys.addin.xml
 src/Extensions/Banshee.NotificationArea/Banshee.NotificationArea.addin.xml
 src/Extensions/Banshee.NotificationArea/Banshee.NotificationArea/NotificationAreaService.cs
@@ -296,4 +302,3 @@ src/Libraries/Lastfm/Lastfm/AudioscrobblerConnection.cs
 src/Libraries/Lastfm/Lastfm/RadioConnection.cs
 src/Libraries/Migo/Migo.Syndication/Feed.cs
 src/Libraries/Migo/Migo.Syndication/RssParser.cs
-src/Extensions/Banshee.MiroGuide/Banshee.MiroGuide/MiroGuideView.cs
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup.addin.xml b/src/Extensions/Banshee.Fixup/Banshee.Fixup.addin.xml
new file mode 100644
index 0000000..ad663a9
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup.addin.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Addin 
+    id="Banshee.Fixup"
+    version="1.0"
+    compatVersion="1.0"
+    copyright="Copyright 2009-2010 Novell Inc. Licensed under the MIT X11 license."
+    name="Metadata Fixup"
+    category="Utilities"
+    description="Fix broken and missing metadata using bulk operations"
+    author="Gabriel Burt"
+    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/ThickClient/ActionGroup">
+    <ActionGroup class="Banshee.Fixup.FixActions"/>
+  </Extension>
+
+  <Extension path="/Banshee/ThickClient/SourceContentProvider">
+    <Provider class="Banshee.Fixup.FixContentProvider"/>
+  </Extension>
+
+  <ExtensionPoint path="/Banshee/MetadataFix/Solver">
+    <ExtensionNode name="Solver"/>
+  </ExtensionPoint>
+
+  <Extension path="/Banshee/MetadataFix/Solver">
+    <Solver class="Banshee.Fixup.ArtistDuplicateSolver"/>
+    <Solver class="Banshee.Fixup.AlbumDuplicateSolver"/>
+    <!--<Solver class="Banshee.Fixup.CompilationSolver"/>-->
+  </Extension>
+
+</Addin>
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup.csproj b/src/Extensions/Banshee.Fixup/Banshee.Fixup.csproj
new file mode 100644
index 0000000..9bd531a
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup.csproj
@@ -0,0 +1,106 @@
+<?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>{9A8BCA22-82D6-4106-961A-DB4F77937BD5}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <UseParentDirectoryAsNamespace>true</UseParentDirectoryAsNamespace>
+    <AssemblyName>Banshee.Fixup</AssemblyName>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ReleaseVersion>1.3</ReleaseVersion>
+    <RootNamespace>Banshee.Fixup</RootNamespace>
+    <AssemblyOriginatorKeyFile>.</AssemblyOriginatorKeyFile>
+    <TargetFrameworkVersion>v3.5</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>
+  </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="..\..\Core\Banshee.ThickClient\Banshee.ThickClient.csproj">
+      <Project>{AC839523-7BDF-4AB6-8115-E17921B96EC6}</Project>
+      <Name>Banshee.ThickClient</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Hyena\Hyena\Hyena.csproj">
+      <Project>{95374549-9553-4C1E-9D89-667755F90E12}</Project>
+      <Name>Hyena</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Hyena\Hyena.Gui\Hyena.Gui.csproj">
+      <Project>{C856EFD8-E812-4E61-8B76-E3583D94C233}</Project>
+      <Name>Hyena.Gui</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Hyena\Hyena.Data.Sqlite\Hyena.Data.Sqlite.csproj">
+      <Project>{95374549-9553-4C1E-9D89-667755F90E13}</Project>
+      <Name>Hyena.Data.Sqlite</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\..\Core\Banshee.Widgets\Banshee.Widgets.csproj">
+      <Project>{A3701765-E571-413D-808C-9788A22791AF}</Project>
+      <Name>Banshee.Widgets</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
+    <Reference Include="System.Core" />
+    <Reference Include="gdk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f" />
+    <Reference Include="System" />
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Banshee.Fixup.addin.xml">
+      <LogicalName>Banshee.Fixup.addin.xml</LogicalName>
+    </EmbeddedResource>
+    <EmbeddedResource Include="Resources\ActiveUI.xml">
+      <LogicalName>ActiveUI.xml</LogicalName>
+    </EmbeddedResource>
+    <EmbeddedResource Include="Resources\GlobalUI.xml">
+      <LogicalName>GlobalUI.xml</LogicalName>
+    </EmbeddedResource>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Banshee.Fixup\AlbumDuplicateSolver.cs" />
+    <Compile Include="Banshee.Fixup\ArtistDuplicateSolver.cs" />
+    <Compile Include="Banshee.Fixup\ColumnCellSolutionOptions.cs" />
+    <Compile Include="Banshee.Fixup\FixActions.cs" />
+    <Compile Include="Banshee.Fixup\FixSource.cs" />
+    <Compile Include="Banshee.Fixup\Problem.cs" />
+    <Compile Include="Banshee.Fixup\ProblemModel.cs" />
+    <Compile Include="Banshee.Fixup\View.cs" />
+    <Compile Include="Banshee.Fixup\Solver.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>
+</Project>
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup/AlbumDuplicateSolver.cs b/src/Extensions/Banshee.Fixup/Banshee.Fixup/AlbumDuplicateSolver.cs
new file mode 100644
index 0000000..32bec74
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup/AlbumDuplicateSolver.cs
@@ -0,0 +1,105 @@
+//
+// AlbumDuplicateSolver.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright 2010 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.Linq;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Hyena;
+using Hyena.Data.Sqlite;
+
+using Banshee.ServiceStack;
+using Banshee.Configuration;
+
+namespace Banshee.Fixup
+{
+    public class AlbumDuplicateSolver : DuplicateSolver
+    {
+        public AlbumDuplicateSolver ()
+        {
+            Id = "dupe-album";
+            Name = Catalog.GetString ("Duplicate Albums");
+            ShortDescription = Catalog.GetString ("");
+            LongDescription = Catalog.GetString ("Displayed are albums that should likely be merged.  For each row, click the desired title to make it bold, or uncheck it to take no action.");
+            Action = Catalog.GetString ("");
+            OptionsTitle = Catalog.GetString ("Duplicate Albums");
+            Order = 10;
+
+            AddFinder (
+                "Title", "AlbumID", "CoreAlbums, CoreArtists",
+                "CoreAlbums.ArtistID = CoreArtists.ArtistID AND Title IS NOT NULL AND Name IS NOT NULL AND " +
+                    String.Format ("AlbumID IN (SELECT DISTINCT(AlbumID) FROM CoreTracks WHERE PrimarySourceID = {0})", ServiceManager.SourceManager.MusicLibrary.DbId),
+                "HYENA_BINARY_FUNCTION ('dupe-album', Title, Name)"
+            );
+
+            BinaryFunction.Add (Id, NormalizedGroup);
+        }
+
+        public override void Dispose ()
+        {
+            BinaryFunction.Remove (Id);
+        }
+
+        private object NormalizedGroup (object album, object artist)
+        {
+            var ret = (album as string);
+            if (ret == null || (artist as string) == null)
+                return null;
+
+            ret.ToLower ()
+               .Replace (" and ", " & ")
+               .Replace (Catalog.GetString (" and "), " & ")
+               .Replace (", the", "")
+               .Replace (Catalog.GetString (", the"), "")
+               .Replace ("the ", "")
+               .Replace (Catalog.GetString ("the "), "")
+               .Trim ();
+
+            // Stips whitespace, punctuation, accents, and lower-cases
+            ret = Hyena.StringUtil.SearchKey (ret);
+            return ret + artist;
+        }
+
+        public override void Fix (IEnumerable<Problem> problems)
+        {
+            foreach (var problem in problems) {
+                // OK, we're combining two or more albums into one.  To do that,
+                // we need to associate all the tracks with the one album
+                // that will remain -- the one with Title == problem.SolutionValue.
+                // So, separate the ID of the winner from the rest
+                var winner_id = problem.ObjectIds [Array.IndexOf (problem.SolutionOptions, problem.SolutionValue)];
+                var losers = problem.ObjectIds.Where (id => id != winner_id).ToArray ();
+
+                ServiceManager.DbConnection.Execute (
+                    "UPDATE CoreTracks SET AlbumID = ? WHERE AlbumID IN (?)",
+                    winner_id, losers
+                );
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup/ArtistDuplicateSolver.cs b/src/Extensions/Banshee.Fixup/Banshee.Fixup/ArtistDuplicateSolver.cs
new file mode 100644
index 0000000..12a73d9
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup/ArtistDuplicateSolver.cs
@@ -0,0 +1,109 @@
+//
+// ArtistDuplicateSolver.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright 2010 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.Linq;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Hyena;
+using Hyena.Data.Sqlite;
+
+using Banshee.ServiceStack;
+using Banshee.Configuration;
+
+namespace Banshee.Fixup
+{
+    public class ArtistDuplicateSolver : DuplicateSolver
+    {
+        public ArtistDuplicateSolver ()
+        {
+            Id = "dupe-artist";
+            Name = Catalog.GetString ("Duplicate Artists");
+            ShortDescription = Catalog.GetString ("");
+            LongDescription = Catalog.GetString ("Displayed are artists that should likely be merged.  For each row, click the desired name to make it bold, or uncheck it to take no action.");
+            Action = Catalog.GetString ("");
+            OptionsTitle = Catalog.GetString ("Duplicate Artists");
+            Order = 5;
+
+            AddFinder (
+                "Name", "ArtistID", "CoreArtists",
+                String.Format (
+                    @"(Name IS NOT NULL AND ArtistID IN (SELECT DISTINCT(ArtistID) FROM CoreTracks WHERE PrimarySourceID = {0})
+                        OR ArtistID IN (SELECT DISTINCT(a.ArtistID) FROM CoreTracks t, CoreAlbums a WHERE t.AlbumID = a.AlbumID AND t.PrimarySourceID = {0}))",
+                    ServiceManager.SourceManager.MusicLibrary.DbId
+                ),
+                "HYENA_BINARY_FUNCTION ('dupe-artist', Name, NULL)"
+            );
+
+            BinaryFunction.Add (Id, NormalizeArtistName);
+        }
+
+        public override void Dispose ()
+        {
+            BinaryFunction.Remove (Id);
+        }
+
+        private object NormalizeArtistName (object name, object null_arg)
+        {
+            var ret = name as string;
+            if (ret == null)
+                return null;
+
+            ret.ToLower ()
+               .Replace (" and ", " & ")
+               .Replace (Catalog.GetString (" and "), " & ")
+               .Replace (", the", "")
+               .Replace (Catalog.GetString (", the"), "")
+               .Replace ("the ", "")
+               .Replace (Catalog.GetString ("the "), "")
+               .Trim ();
+
+            // Stips whitespace, punctuation, accents, and lower-cases
+            ret = Hyena.StringUtil.SearchKey (ret);
+            return ret;
+        }
+
+        public override void Fix (IEnumerable<Problem> problems)
+        {
+            foreach (var problem in problems) {
+                // OK, we're combining two or more artists into one.  To do that,
+                // we need to associate all the tracks and albums onto the the
+                // that will remain -- the one with Name == problem.SolutionValue.
+                // So, separate the ID of the winner from the rest
+                var winner_id = problem.ObjectIds [Array.IndexOf (problem.SolutionOptions, problem.SolutionValue)];
+                var losers = problem.ObjectIds.Where (id => id != winner_id).ToArray ();
+
+                ServiceManager.DbConnection.Execute (
+                    @"UPDATE CoreAlbums SET ArtistID = ? WHERE ArtistID IN (?);
+                      UPDATE CoreTracks SET ArtistID = ? WHERE ArtistID IN (?)",
+                    winner_id, losers, winner_id, losers
+                );
+            }
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup/ColumnCellSolutionOptions.cs b/src/Extensions/Banshee.Fixup/Banshee.Fixup/ColumnCellSolutionOptions.cs
new file mode 100644
index 0000000..6cdc466
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup/ColumnCellSolutionOptions.cs
@@ -0,0 +1,129 @@
+//
+// ColumnCellSolutionOptions.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright (C) 2009-2010 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.Linq;
+
+using Mono.Unix;
+
+using Hyena;
+using Hyena.Data.Gui;
+using System.Text;
+using System.Collections.Generic;
+
+namespace Banshee.Fixup
+{
+    public class ColumnCellSolutionOptions : ColumnCellText, IInteractiveCell
+    {
+        bool measure;
+        List<int> solution_value_widths = new List<int> ();
+
+        public ColumnCellSolutionOptions () : base (null, true)
+        {
+            UseMarkup = true;
+        }
+
+        public override void Render (CellContext context, Gtk.StateType state, double cellWidth, double cellHeight)
+        {
+            base.Render (context, state, cellWidth, cellHeight);
+
+            if (measure) {
+                solution_value_widths.Clear ();
+                var sb = new StringBuilder ();
+                int x = 0, w, h;
+                foreach (var str in SolutionOptions) {
+                    sb.Append (str);
+                    context.Layout.SetMarkup (sb.ToString ());
+                    context.Layout.GetPixelSize (out w, out h);
+                    x += w;
+                    solution_value_widths.Add (x);
+                    sb.Append (solution_joiner);
+                }
+            }
+        }
+
+        private IEnumerable<string> SolutionOptions {
+            get {
+                var problem = (Problem)BoundObject;
+                return problem.SolutionOptions
+                               .Select (o => o == problem.SolutionValue
+                                   ? String.Format ("<b>{0}</b>", GLib.Markup.EscapeText (o))
+                                   : GLib.Markup.EscapeText (o));
+            }
+        }
+
+        const string solution_joiner = ", ";
+
+        protected override string GetText (object obj)
+        {
+            return SolutionOptions.Join (solution_joiner);
+        }
+
+        private int GetSolutionValueFor (int x)
+        {
+            if (solution_value_widths.Count == 0)
+                return -1;
+
+            int cur_x = 0;
+            for (int i = 0; i < solution_value_widths.Count; i++) {
+                cur_x += solution_value_widths[i];
+                if (x < cur_x)
+                    return i;
+            }
+            return -1;
+        }
+
+        public bool ButtonEvent (int x, int y, bool pressed, Gdk.EventButton press)
+        {
+            if (press.Button == 1 && press.Type == Gdk.EventType.ButtonRelease) {
+                int sol = GetSolutionValueFor (x);
+                if (sol != -1) {
+                    var problem = ((Problem)BoundObject);
+                    problem.SolutionValue = problem.SolutionOptions.Skip (sol).First ();
+                    Problem.Provider.Save (problem);
+                    ProblemModel.Instance.Reload ();
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        public bool MotionEvent (int x, int y, Gdk.EventMotion evnt)
+        {
+            measure = true;
+            return false;
+        }
+
+        public bool PointerLeaveEvent ()
+        {
+            solution_value_widths.Clear ();
+            measure = false;
+            return false;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup/FixActions.cs b/src/Extensions/Banshee.Fixup/Banshee.Fixup/FixActions.cs
new file mode 100644
index 0000000..f221415
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup/FixActions.cs
@@ -0,0 +1,72 @@
+//
+// FixActions.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright 2010 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.Linq;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Gtk;
+
+using Hyena;
+using Hyena.Data;
+using Hyena.Data.Sqlite;
+
+using Banshee.ServiceStack;
+using Banshee.Gui;
+
+namespace Banshee.Fixup
+{
+    public class FixActions : BansheeActionGroup
+    {
+        public FixActions () : base ("MetadataFixActions")
+        {
+            Add (new Gtk.ActionEntry (
+                "FixMetadataAction", null,
+                Catalog.GetString ("Fix Music Metadata..."), null,
+                null, OnFixMetadata
+            ));
+
+            AddUiFromFile ("GlobalUI.xml");
+        }
+
+        private void OnFixMetadata (object o, EventArgs args)
+        {
+            var music = ServiceManager.SourceManager.MusicLibrary;
+
+            // Only one fix source at a time
+            if (music.Children.Any (c => c is FixSource)) {
+                ServiceManager.SourceManager.SetActiveSource (music.Children.First (c => c is FixSource));
+                return;
+            }
+
+            var src = new FixSource ();
+            music.AddChildSource (src);
+            ServiceManager.SourceManager.SetActiveSource (src);
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup/FixSource.cs b/src/Extensions/Banshee.Fixup/Banshee.Fixup/FixSource.cs
new file mode 100644
index 0000000..e9be1e1
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup/FixSource.cs
@@ -0,0 +1,104 @@
+//
+// FixSource.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright 2010 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;
+using Hyena.Data;
+using Hyena.Data.Sqlite;
+
+using Banshee.ServiceStack;
+using Banshee.Gui;
+using Banshee.Sources;
+
+namespace Banshee.Fixup
+{
+    public class FixSource : Source, IUnmapableSource
+    {
+        ProblemModel problem_model = new ProblemModel ();
+
+        public FixSource () : base (Catalog.GetString ("Metadata Fixer"), Catalog.GetString ("Metadata Fixer"), 0)
+        {
+            TypeUniqueId = "fixes";
+
+            var header_widget = new HBox () { Spacing = 6 };
+
+            header_widget.PackStart (new Label (Catalog.GetString ("Problem Type:")), false, false, 0);
+
+            var combo = new Banshee.Widgets.DictionaryComboBox<Solver> ();
+            foreach (var solver in problem_model.Solvers) {
+                combo.Add (solver.Name, solver);
+            }
+            combo.Changed += (o, a) => {
+                problem_model.Solver = combo.ActiveValue;
+                SetStatus (problem_model.Solver.LongDescription, false, false, "gtk-info");
+            };
+            combo.Active = 0;
+
+            var apply_button = new Hyena.Widgets.ImageButton ("Apply Selected Fixes", "gtk-apply");
+            apply_button.Clicked += (o, a) => problem_model.Fix ();
+            problem_model.Reloaded += (o, a) => apply_button.Sensitive = problem_model.SelectedCount > 0;
+
+            header_widget.PackStart (combo, false, false, 0);
+            header_widget.PackStart (apply_button, false, false, 0);
+            header_widget.ShowAll ();
+
+            Properties.SetStringList ("Icon.Name", "search", "gtk-search");
+            Properties.SetString ("ActiveSourceUIResource", "ActiveUI.xml");
+            Properties.SetString ("GtkActionPath", "/FixSourcePopup");
+            Properties.Set<Gtk.Widget> ("Nereid.SourceContents.HeaderWidget", header_widget);
+            Properties.Set<Banshee.Sources.Gui.ISourceContents> ("Nereid.SourceContents", new View (problem_model));
+            Properties.SetString ("UnmapSourceActionLabel", Catalog.GetString ("Close"));
+            Properties.SetString ("UnmapSourceActionIconName", "gtk-close");
+
+            var actions = new BansheeActionGroup ("fix-source");
+            actions.AddImportant (
+                new ActionEntry ("RefreshProblems", Stock.Refresh, Catalog.GetString ("Refresh"), null, null, (o, a) => {
+                    problem_model.Refresh ();
+                })
+            );
+            actions.Register ();
+
+            problem_model.Reload ();
+        }
+
+        public bool CanUnmap { get { return true; } }
+        public bool ConfirmBeforeUnmap { get { return false; } }
+
+        public bool Unmap ()
+        {
+            Parent.RemoveChildSource (this);
+            Properties.Get<Banshee.Sources.Gui.ISourceContents> ("Nereid.SourceContents").Widget.Destroy ();
+            problem_model.Dispose ();
+            return true;
+        }
+    }
+}
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup/Problem.cs b/src/Extensions/Banshee.Fixup/Banshee.Fixup/Problem.cs
new file mode 100644
index 0000000..0b33abd
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup/Problem.cs
@@ -0,0 +1,136 @@
+//
+// Problem.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright 2010 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.Linq;
+using System.Collections.Generic;
+
+using Hyena;
+using Hyena.Data.Sqlite;
+
+using Banshee.ServiceStack;
+
+namespace Banshee.Fixup
+{
+    public class Problem : IEquatable<Problem>
+    {
+        private static SqliteModelProvider<Problem> provider;
+        public static SqliteModelProvider<Problem> Provider {
+            get {
+                return provider ?? (provider =
+                    new SqliteModelProvider<Problem> (ServiceManager.DbConnection, "MetadataProblems", false));
+            }
+        }
+
+        [DatabaseColumn ("ProblemID", Constraints = DatabaseColumnConstraints.PrimaryKey)]
+        public int Id { get; private set; }
+
+        [DatabaseColumn ("ProblemType")]
+        public string ProblemType { get; private set; }
+
+        [DatabaseColumn]
+        public bool Selected { get; set; }
+
+        [DatabaseColumn]
+        public string SolutionValue { get; set; }
+
+        [DatabaseColumn ("SolutionOptions")]
+        private string options_field;
+
+        [DatabaseColumn ("ObjectIds")]
+        internal string object_ids_field;
+
+        [DatabaseColumn]
+        public int ObjectCount { get; private set; }
+
+        private int [] object_ids;
+        public int [] ObjectIds {
+            get {
+                if (object_ids == null && object_ids_field != null) {
+                    object_ids = object_ids_field.Split (',')
+                                                 .Select (i => Int32.Parse (i))
+                                                 .ToArray ();
+                }
+                return object_ids;
+            }
+        }
+
+        private string [] options;
+        public string [] SolutionOptions {
+            get {
+                if (options == null && options_field != null) {
+                    options = options_field.Split (splitter, StringSplitOptions.None);
+                }
+                return options;
+            }
+        }
+
+        public override bool Equals (object b)
+        {
+            return Equals (b as Problem);
+        }
+
+        public bool Equals (Problem b)
+        {
+            return b != null && b.Id == this.Id;
+        }
+
+        public override int GetHashCode ()
+        {
+            return Id;
+        }
+
+        public override string ToString ()
+        {
+            return String.Format ("<Problem Id={2} Type={0}>", Id, ProblemType);
+        }
+
+        public static void Initialize ()
+        {
+            ServiceManager.DbConnection.Execute (@"DROP TABLE IF EXISTS MetadataProblems");
+            if (!ServiceManager.DbConnection.TableExists ("MetadataProblems")) {
+                ServiceManager.DbConnection.Execute (@"
+                    CREATE TABLE MetadataProblems (
+                        ProblemID   INTEGER PRIMARY KEY,
+                        ProblemType TEXT NOT NULL,
+                        TypeOrder   INTEGER NOT NULL,
+                        Generation  INTEGER NOT NULL,
+                        Selected    INTEGER DEFAULT 1,
+
+                        SolutionValue       TEXT,
+                        SolutionOptions     TEXT,
+                        ObjectIds   TEXT,
+                        ObjectCount INTEGER,
+
+                        UNIQUE (ProblemType, Generation, ObjectIds) ON CONFLICT IGNORE
+                    )"
+                );
+            }
+        }
+
+        private static string [] splitter = new string [] { ";;" };
+    }
+}
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup/ProblemModel.cs b/src/Extensions/Banshee.Fixup/Banshee.Fixup/ProblemModel.cs
new file mode 100644
index 0000000..1add9d0
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup/ProblemModel.cs
@@ -0,0 +1,186 @@
+//
+// ProblemModel.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright 2010 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 Mono.Addins;
+
+using Gtk;
+
+using Hyena;
+using Hyena.Data;
+using Hyena.Data.Sqlite;
+
+using Banshee.ServiceStack;
+using Banshee.Preferences;
+using Banshee.Configuration;
+
+using Banshee.Gui;
+
+namespace Banshee.Fixup
+{
+    public class ProblemModel : BaseListModel<Problem>
+    {
+        private static ProblemModel instance;
+        public static ProblemModel Instance {
+            get { return instance; }
+        }
+
+        private int count;
+        private int selected_count;
+        private List<Solver> solvers = new List<Solver> ();
+        private Dictionary<string, Solver> solvers_hash = new Dictionary<string, Solver> ();
+
+        public ProblemModel ()
+        {
+            instance = this;
+            Selection = new Hyena.Collections.Selection ();
+
+            Problem.Initialize ();
+
+            AddSolvers ();
+        }
+
+        public void Dispose ()
+        {
+            foreach (var solver in Solvers) {
+                solver.Dispose ();
+            }
+        }
+
+        public IEnumerable<Solver> Solvers { get { return solvers; } }
+
+        private Solver solver;
+        public Solver Solver {
+            get { return solver; }
+            set {
+                if (value == solver)
+                    return;
+
+                solver = value;
+                Clear ();
+                Refresh ();
+                Log.DebugFormat ("Metadata Solver changed to {0}", solver.Name);
+            }
+        }
+
+        private void AddSolvers ()
+        {
+            Solver solver = null;
+            foreach (TypeExtensionNode node in AddinManager.GetExtensionNodes ("/Banshee/MetadataFix/Solver")) {
+                try {
+                    solver = (Solver) node.CreateInstance (typeof (Solver));
+                } catch (Exception e) {
+                    Log.Exception (e);
+                    continue;
+                }
+
+                Add (solver);
+            }
+        }
+
+        private void Add (Solver solver)
+        {
+            solvers.Add (solver);
+            solvers_hash[solver.Id] = solver;
+        }
+
+        public Solver GetSolverFor (Problem fixable)
+        {
+            if (solvers_hash.ContainsKey (fixable.ProblemType)) {
+                return solvers_hash[fixable.ProblemType];
+            }
+            return null;
+        }
+
+        public void Refresh ()
+        {
+            Solver.FindProblems ();
+            Reload ();
+        }
+
+        public void Fix ()
+        {
+            Solver.FixSelected ();
+            ServiceManager.SourceManager.MusicLibrary.NotifyTracksChanged ();
+            Refresh ();
+        }
+
+#region BaseListModel implementation
+
+        public override void Clear ()
+        {
+            count = 0;
+            selected_count = 0;
+            ServiceManager.DbConnection.Execute ("DELETE FROM MetadataProblems");
+            OnCleared ();
+        }
+
+        public void ToggleSelection ()
+        {
+            foreach (var range in Selection.Ranges) {
+                ServiceManager.DbConnection.Execute (
+                    @"UPDATE MetadataProblems SET Selected = NOT(Selected) WHERE ProblemID IN
+                        (SELECT ProblemID FROM MetadataProblems ORDER BY ProblemID LIMIT ?, ?)",
+                    range.Start, range.End - range.Start + 1);
+            }
+            Reload ();
+        }
+
+        public override void Reload ()
+        {
+            count = ServiceManager.DbConnection.Query<int> ("SELECT count(*) FROM MetadataProblems");
+            selected_count = ServiceManager.DbConnection.Query<int> ("SELECT count(*) FROM MetadataProblems WHERE Selected = 1");
+            OnReloaded ();
+        }
+
+        public override Problem this[int index] {
+            get {
+                lock (this) {
+                    foreach (Problem fixable in Problem.Provider.FetchRange (index, 1)) {
+                        return fixable;
+                    }
+
+                    return null;
+                }
+            }
+        }
+
+        public override int Count {
+            get { return count; }
+        }
+
+#endregion
+
+        public int SelectedCount {
+            get { return selected_count; }
+        }
+
+    }
+}
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup/Solver.cs b/src/Extensions/Banshee.Fixup/Banshee.Fixup/Solver.cs
new file mode 100644
index 0000000..92f307e
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup/Solver.cs
@@ -0,0 +1,218 @@
+//
+// Solver.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright 2010 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.Linq;
+using System.Collections.Generic;
+
+using Mono.Unix;
+
+using Hyena;
+using Hyena.Data.Sqlite;
+
+using Banshee.ServiceStack;
+using Banshee.Configuration;
+
+namespace Banshee.Fixup
+{
+    public abstract class Solver : IDisposable
+    {
+        private string id;
+
+        /* Find the highest TrackNumber for albums where not all tracks have it set */
+        //SELECT AlbumID, Max(TrackCount) as MaxTrackNum FROM CoreTracks GROUP BY AlbumID HAVING MaxTrackNum > 0 AND MaxTrackNum != Min(TrackCount);
+
+        public Solver ()
+        {
+            Order = 100;
+        }
+
+        public string Id {
+            get { return id; }
+            set {
+                if (id != null) {
+                    throw new InvalidOperationException ("Solver's Id is already set; can't change it");
+                }
+
+                id = value;
+                Generation = DatabaseConfigurationClient.Client.Get<int> ("MetadataFixupGeneration", id, 0);
+            }
+        }
+
+        public string Action { get; set; }
+        public string Name { get; set; }
+        public string ShortDescription { get; set; }
+        public string LongDescription { get; set; }
+        public string OptionsTitle { get; set; }
+        public int Order { get; set; }
+        public int Generation { get; private set; }
+
+        public void FindProblems ()
+        {
+            // Bump the generation number
+            Generation++;
+            DatabaseConfigurationClient.Client.Set<int> ("MetadataFixupGeneration", Id, Generation);
+
+            // Identify the new issues
+            IdentifyCore ();
+
+            // Unselect any problems that the user had previously unselected
+            ServiceManager.DbConnection.Execute (
+                @"UPDATE MetadataProblems SET Selected = 0 WHERE ProblemType = ? AND Generation = ? AND ObjectIds IN
+                    (SELECT ObjectIds FROM MetadataProblems WHERE ProblemType = ? AND Generation = ? AND Selected = 0)",
+                Id, Generation, Id, Generation - 1
+            );
+
+            // Delete the previous generation's issues
+            ServiceManager.DbConnection.Execute (
+                "DELETE FROM MetadataProblems WHERE ProblemType = ? AND Generation = ?",
+                Id, Generation - 1
+            );
+        }
+
+        public virtual void Dispose () {}
+
+        public void FixSelected ()
+        {
+            Fix (Problem.Provider.FetchAllMatching ("Selected = 1"));
+        }
+
+        protected abstract void IdentifyCore ();
+        public abstract void Fix (IEnumerable<Problem> problems);
+    }
+
+    public abstract class DuplicateSolver : Solver
+    {
+        private List<HyenaSqliteCommand> find_cmds = new List<HyenaSqliteCommand> ();
+
+        public void AddFinder (string value_column, string id_column, string from, string condition, string group_by)
+        {
+            /* The val result SQL gives us the first/highest value (in descending
+             * sort order), so Foo Fighters over foo fighters.  Except it ignore all caps
+             * ASCII values, so given the values Foo, FOO, and foo, they sort as
+             * FOO, Foo, and foo, but we ignore FOO and pick Foo.  But because sqlite's
+             * lower/upper functions only work for ASCII, our check for whether the
+             * value is all uppercase involves ensuring that it doesn't also appear to be
+             * lower case (that is, it may have zero ASCII characters).
+             *
+             * TODO: replace with a custom SQLite function
+             *
+             */
+            find_cmds.Add (new HyenaSqliteCommand (String.Format (@"
+                    INSERT INTO MetadataProblems (ProblemType, TypeOrder, Generation, SolutionValue, SolutionOptions, ObjectIds, ObjectCount)
+                    SELECT
+                        '{0}', {1}, {2},
+                        COALESCE (
+                            NULLIF (
+                                MIN(CASE (upper({3}) = {3} AND NOT lower({3}) = {3})
+                                    WHEN 1 THEN '~~~'
+                                    ELSE {3} END),
+                                '~~~'),
+                            {3}) as val,
+                        substr(group_concat({3}, ';;'), 1),
+                        substr(group_concat({4}, ','), 1),
+                        count(*) as num
+                    FROM {5}
+                    WHERE {6}
+                    GROUP BY {7} HAVING num > 1
+                    ORDER BY {3}",
+                Id, Order, "?", // ? is for the Generation variable, which changes
+                value_column, id_column, from, condition ?? "1=1", group_by))
+            );
+        }
+
+        protected override void IdentifyCore ()
+        {
+            ServiceManager.DbConnection.Execute (@"
+                DELETE FROM CoreAlbums WHERE AlbumID NOT IN (SELECT DISTINCT(AlbumID) FROM CoreTracks);
+                DELETE FROM CoreArtists WHERE
+                    ArtistID NOT IN (SELECT DISTINCT(ArtistID) FROM CoreTracks) AND
+                    ArtistID NOT IN (SELECT DISTINCT(ArtistID) FROM CoreAlbums WHERE ArtistID IS NOT NULL);"
+            );
+
+            foreach (HyenaSqliteCommand cmd in find_cmds) {
+                ServiceManager.DbConnection.Execute (cmd, Generation);
+            }
+        }
+    }
+
+    /*public class CompilationSolver : Solver
+    {
+        private HyenaSqliteCommand find_cmd;
+
+        public CompilationSolver ()
+        {
+            Id = "make-compilation";
+            Name = Catalog.GetString ("Compilation Albums");
+            ShortDescription = Catalog.GetString ("Find albums that should be marked as compilation albums");
+            LongDescription = Catalog.GetString ("Find albums that should be marked as compilation albums but are not");
+            Action = Catalog.GetString ("Mark as compilation");
+            Order = 20;
+
+            find_cmd = new HyenaSqliteCommand (String.Format (@"
+                INSERT INTO MetadataProblems (ProblemType, TypeOrder, Generation, SolutionValue, Options, Summary, Count)
+                SELECT
+                    '{0}', {1}, {2},
+                    a.Title, a.Title, a.Title, count(*) as numtracks
+                FROM
+                    CoreTracks t,
+                    CoreAlbums a
+                WHERE
+                    t.PrimarySourceID = 1 AND
+                    a.IsCompilation = 0 AND
+                    t.AlbumID = a.AlbumID
+                GROUP BY
+                    a.Title
+                HAVING
+                    numtracks > 1 AND
+                    t.TrackCount = {3} AND
+                    a.Title != 'Unknown Album' AND
+                    a.Title != 'title' AND
+                    a.Title != 'no title' AND
+                    a.Title != 'Album' AND
+                    a.Title != 'Music' AND (
+                            {5} > 1 AND {5} = {4} AND (
+                            {3} = 0 OR ({3} >= {5}
+                                AND {3} >= numtracks))
+                        OR lower(a.Title) LIKE '%soundtrack%'
+                        OR lower(a.Title) LIKE '%soundtrack%'
+                    )",
+                Id, Order, Generation,
+                "max(t.TrackCount)", "count(distinct(t.artistid))", "count(distinct(t.albumid))"
+            ));
+        }
+
+        protected override void IdentifyCore ()
+        {
+            ServiceManager.DbConnection.Execute (find_cmd);
+        }
+
+        public override void Fix (IEnumerable<Problem> problems)
+        {
+            Console.WriteLine ("Asked to fix compilations..");
+        }
+    }*/
+}
diff --git a/src/Extensions/Banshee.Fixup/Banshee.Fixup/View.cs b/src/Extensions/Banshee.Fixup/Banshee.Fixup/View.cs
new file mode 100644
index 0000000..2a9ebec
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Banshee.Fixup/View.cs
@@ -0,0 +1,133 @@
+//
+// View.cs
+//
+// Author:
+//   Gabriel Burt <gburt novell com>
+//
+// Copyright 2010 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;
+using Hyena.Data;
+using Hyena.Data.Gui;
+using Hyena.Data.Sqlite;
+
+using Hyena.Widgets;
+
+using Banshee.ServiceStack;
+using Banshee.Sources;
+using Banshee.Library;
+
+using Banshee.Gui;
+using Banshee.Sources.Gui;
+using Banshee.Preferences.Gui;
+
+namespace Banshee.Fixup
+{
+    public class View : RoundedFrame, ISourceContents
+    {
+        public View (ProblemModel model)
+        {
+            var view = new ProblemListView (model);
+            var sw = new Gtk.ScrolledWindow () {
+                ShadowType = ShadowType.None,
+                BorderWidth = 0
+            };
+            sw.Add (view);
+
+            Add (sw);
+            ShowAll ();
+        }
+
+        private class ProblemListView : ListView<Problem>
+        {
+            ProblemModel model;
+            public ProblemListView (ProblemModel model)
+            {
+                this.model = model;
+                SetModel (model);
+                ColumnController = new ColumnController ();
+
+                var selected = new ColumnCellCheckBox ("Selected", true);
+                ColumnController.Add (new Column (Catalog.GetString ("Fix?"), selected, 0));
+
+                var summary = new ColumnCellSolutionOptions ();
+                var summary_col = new Column ("", summary, 1.0);
+                ColumnController.Add (summary_col);
+                model.Reloaded += (o, a) => summary_col.Title = model.Solver.OptionsTitle;
+
+                RowOpaquePropertyName = "Selected";
+                RulesHint = true;
+                RowActivated += (o, e) => model.ToggleSelection ();
+            }
+
+            protected override bool OnKeyPressEvent (Gdk.EventKey press)
+            {
+                switch (press.Key) {
+                    case Gdk.Key.space:
+                    case Gdk.Key.Return:
+                    case Gdk.Key.KP_Enter:
+                        model.ToggleSelection ();
+                        return true;
+                }
+
+                return base.OnKeyPressEvent (press);
+            }
+
+            protected override bool OnPopupMenu ()
+            {
+                // TODO add a context menu w/ Select and Unselect options
+                //ServiceManager.Get<InterfaceActionService> ().TrackActions["TrackContextMenuAction"].Activate ();
+                return true;
+            }
+        }
+
+#region ISourceContents
+
+        private MusicLibrarySource source;
+        public bool SetSource (ISource source)
+        {
+            this.source = source as MusicLibrarySource;
+            return this.source != null;
+        }
+
+        public ISource Source {
+            get { return source; }
+        }
+
+        public void ResetSource ()
+        {
+            source = null;
+        }
+
+        public Widget Widget {
+            get { return this; }
+        }
+
+#endregion
+    }
+}
diff --git a/src/Extensions/Banshee.Fixup/Makefile.am b/src/Extensions/Banshee.Fixup/Makefile.am
new file mode 100644
index 0000000..67c51f3
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Makefile.am
@@ -0,0 +1,23 @@
+ASSEMBLY = Banshee.Fixup
+TARGET = library
+LINK = $(REF_EXTENSION_FIXUP)
+INSTALL_DIR = $(EXTENSIONS_INSTALL_DIR)
+
+SOURCES =  \
+	Banshee.Fixup/AlbumDuplicateSolver.cs \
+	Banshee.Fixup/ArtistDuplicateSolver.cs \
+	Banshee.Fixup/ColumnCellSolutionOptions.cs \
+	Banshee.Fixup/FixActions.cs \
+	Banshee.Fixup/FixSource.cs \
+	Banshee.Fixup/Problem.cs \
+	Banshee.Fixup/ProblemModel.cs \
+	Banshee.Fixup/Solver.cs \
+	Banshee.Fixup/View.cs
+
+RESOURCES =  \
+	Banshee.Fixup.addin.xml \
+	Resources/ActiveUI.xml \
+	Resources/GlobalUI.xml
+
+include $(top_srcdir)/build/build.mk
+
diff --git a/src/Extensions/Banshee.Fixup/Resources/ActiveUI.xml b/src/Extensions/Banshee.Fixup/Resources/ActiveUI.xml
new file mode 100644
index 0000000..b49bc81
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Resources/ActiveUI.xml
@@ -0,0 +1,13 @@
+<ui>
+  <toolbar name="HeaderToolbar">
+    <placeholder name="SourceActions">
+        <toolitem action="RefreshProblems" />
+        <toolitem action="UnmapSourceAction" />
+    </placeholder>
+  </toolbar>
+
+  <popup action="FixSourcePopup">
+    <menuitem action="RefreshProblems" />
+    <menuitem action="UnmapSourceAction" />
+  </popup>
+</ui>
diff --git a/src/Extensions/Banshee.Fixup/Resources/GlobalUI.xml b/src/Extensions/Banshee.Fixup/Resources/GlobalUI.xml
new file mode 100644
index 0000000..59ac5a2
--- /dev/null
+++ b/src/Extensions/Banshee.Fixup/Resources/GlobalUI.xml
@@ -0,0 +1,7 @@
+<ui>
+    <menubar name="MainMenu">
+        <menu name="ToolsMenu" action="ToolsMenuAction">
+            <menuitem name="MetadataFix" action="FixMetadataAction" />
+        </menu>
+	</menubar>
+</ui>
diff --git a/src/Extensions/Makefile.am b/src/Extensions/Makefile.am
index 5272f25..28b7cc4 100644
--- a/src/Extensions/Makefile.am
+++ b/src/Extensions/Makefile.am
@@ -10,6 +10,7 @@ SUBDIRS = \
 	Banshee.Emusic \
 	Banshee.FileSystemQueue \
 	Banshee.InternetArchive \
+	Banshee.Fixup \
 	Banshee.InternetRadio \
 	Banshee.Lastfm \
 	Banshee.LastfmStreaming \
diff --git a/src/Hyena b/src/Hyena
index e3198fc..b712d51 160000
--- a/src/Hyena
+++ b/src/Hyena
@@ -1 +1 @@
-Subproject commit e3198fc8dfe306ff2ec4fee00ce2abb43b319889
+Subproject commit b712d518846bfccbbfd92a1ea85ee2d29844a1ff



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