[banshee] [Fixup] New extension for bulk metadata fixing
- From: Gabriel Burt <gburt src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [banshee] [Fixup] New extension for bulk metadata fixing
- Date: Fri, 16 Jul 2010 03:18:58 +0000 (UTC)
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]