In my exploration of Open Source projects of interest I came across this one.
http://www.hanselman.com/blog/RemovingDeadTracksDuplicatesThatDontExistFromITunesUsingC.aspx

So I think iTunes actually does find duplicates now
.. but interesting code:
public partial class Form1 : Form
{
private volatile bool _shouldStop;
“Volatile: Over-simplifying and paraphrasing:
volatile indicates that every read operation needs to re-read from memory because there might be other threads updating the variable.”
http://stackoverflow.com/questions/1186515/interlocked-and-volatile
Maybe dropped in .NET5? http://www.bluebytesoftware.com/blog/2010/12/04/SayonaraVolatile.aspx
Architecture
All in code behind the winform.
ListView with columns
Label1 and a Progress Bar.
checkBoxRemove
Worker is on a different thread to the UI. and is spun up when the button is pressed:
//button1 is find dead tracks
private void button1_Click(object sender, EventArgs e)
{
this._shouldStop = false;
this.buttonCancel.Enabled = true;
this.listView1.Items.Clear();
this.worker = new Thread(this.FindDeadTracks);
this.worker.Start();
}
Helper methods for FindDeadTracks and RemoveDuplicates
SetupProgress(max value eg trackcount) – this sets up the ProgressBar (we’re on the worker thread so have to invoke back to UI).. which magically calls the SetupProgress again and sets the max value of the progress bar
in FindDeadTracks (worker thread)
//setup the progress control
this.SetupProgress(trackCount);
then in class level
delegate void SetupProgressCallback(int max);
private void SetupProgress(int max)
{
if (this.progressBar1.InvokeRequired)
{
SetupProgressCallback cb = new SetupProgressCallback(SetupProgress);
this.Invoke(cb, new object[] { max });
}
else
{
this.progressBar1.Maximum = max;
this.progressBar1.Minimum = 1;
this.progressBar1.Step = 1;
this.progressBar1.Value = 1;
}
}
nifty! worker thread calls it first, which it then goes into the InvokeRequired section. Then the delegate calls it.
IncrementProgress – both worker methods can call this (its already been setup). Same as above.. just increments a step
UpdateLabel – updates label1
FindDeadTrack
goes through and looks for an empty filename or an exception on System.File.IO.Exists
Remove Duplicates
Dictionary for trackCollection. where key string could be: FarewellYngwie MalmsteenRising Force notice there are spaces
var trackCollection = new Dictionary<string, IITTrack>();
The app will never add duplicates into the trackCollection
ArrayList for tracksToRemove
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Collections;
using iTunesLib;
namespace iTunesCOMSample
{
public partial class Form1 : Form
{
private volatile bool _shouldStop;
private Thread worker;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
this.label1.Text = "";
this.buttonCancel.Enabled = false;
}
private void RemoveDuplicates()
{
//create a reference to iTunes
var iTunes = new iTunesAppClass();
//get a reference to the collection of all tracks
IITTrackCollection tracks = iTunes.LibraryPlaylist.Tracks;
int trackCount = tracks.Count;
int numberChecked = 0;
int numberDuplicateFound = 0;
//dictionary is key,value
var trackCollection = new Dictionary<string, IITTrack>();
var tracksToRemove = new ArrayList();
//setup the progress control
this.SetupProgress(trackCount);
for (int i = trackCount; i > 0; i--)
{
if (tracks[i].Kind == ITTrackKind.ITTrackKindFile)
{
if (!this._shouldStop)
{
numberChecked++;
this.IncrementProgress();
this.UpdateLabel("Checking track # " + numberChecked.ToString() + " - " + tracks[i].Name);
//eg key: FarewellYngwie MalmsteenRising Force" notice there are spaces
string trackKey = tracks[i].Name + tracks[i].Artist + tracks[i].Album;
if (!trackCollection.ContainsKey(trackKey))
trackCollection.Add(trackKey, tracks[i]);
else //if the trackCollection does have this song already
{
//if not in the same album or not by the same artist?
if (trackCollection[trackKey].Album != tracks[i].Album || trackCollection[trackKey].Artist != tracks[i].Artist)
trackCollection.Add(trackKey, tracks[i]);
//if track in collection has a higherbitrate than current
else if (trackCollection[trackKey].BitRate > tracks[i].BitRate)
{
IITFileOrCDTrack fileTrack = (IITFileOrCDTrack)tracks[i];
numberDuplicateFound++;
tracksToRemove.Add(tracks[i]);
}
//bitrate is higher in this one so replace the one in trackCollection with this version.
//default just replace the existing track with this one
else
{
IITFileOrCDTrack fileTrack = (IITFileOrCDTrack)tracks[i];
trackCollection[trackKey] = fileTrack;
numberDuplicateFound++;
tracksToRemove.Add(tracks[i]);
}
}
}
}
}
this.SetupProgress(tracksToRemove.Count);
//tracksToRemove is an ArrayList
for (int i = 0; i < tracksToRemove.Count; i++)
{
IITFileOrCDTrack track = (IITFileOrCDTrack)tracksToRemove[i];
this.UpdateLabel("Removing " + track.Name);
this.IncrementProgress();
this.AddTrackToList((IITFileOrCDTrack)tracksToRemove[i]);
if (this.checkBoxRemove.Checked)
track.Delete();
}
this.UpdateLabel("Checked " + numberChecked.ToString() + " tracks and " + numberDuplicateFound.ToString() + " duplicate tracks found.");
this.SetupProgress(1);
}
private void FindDeadTracks()
{
//create a reference to iTunes
iTunesAppClass iTunes = new iTunesAppClass();
//get a reference to the collection of all tracks
IITTrackCollection tracks = iTunes.LibraryPlaylist.Tracks;
int trackCount = tracks.Count;
int numberChecked = 0;
int numberDeadFound = 0;
//setup the progress control
this.SetupProgress(trackCount);
for (int i = trackCount; i > 0; i--)
{
if (!this._shouldStop)
{
IITTrack track = tracks[i];
numberChecked++;
this.IncrementProgress();
this.UpdateLabel("Checking track # " + numberChecked.ToString() + " - " + track.Name);
if (track.Kind == ITTrackKind.ITTrackKindFile)
{
IITFileOrCDTrack fileTrack = (IITFileOrCDTrack)track;
//if the file doesn't exist, we'll delete it from iTunes
if (fileTrack.Location == String.Empty)
{
numberDeadFound++;
this.AddTrackToList(fileTrack);
if (this.checkBoxRemove.Checked)
fileTrack.Delete();
}
else if (!System.IO.File.Exists(fileTrack.Location))
{
numberDeadFound++;
this.AddTrackToList(fileTrack);
if (this.checkBoxRemove.Checked)
fileTrack.Delete();
}
}
}
}
this.UpdateLabel("Checked " + numberChecked.ToString() + " tracks and " + numberDeadFound.ToString() + " dead tracks found.");
//sets the progressbar back to nothing
this.SetupProgress(1);
}
#region Button clicks
private void buttonCancel_Click(object sender, EventArgs e)
{
this._shouldStop = true;
this.buttonCancel.Enabled = false;
}
//button1 is find dead tracks
private void button1_Click(object sender, EventArgs e)
{
this._shouldStop = false;
this.buttonCancel.Enabled = true;
this.listView1.Items.Clear();
this.worker = new Thread(this.FindDeadTracks);
this.worker.Start();
}
//button2 is duplicates
private void button2_Click(object sender, EventArgs e)
{
this._shouldStop = false;
this.buttonCancel.Enabled = true;
this.listView1.Items.Clear();
this.worker = new Thread(this.RemoveDuplicates);
this.worker.Start();
}
#endregion
#region Delegate Callbacks
//delagates for thread-safe access to UI components
delegate void SetupProgressCallback(int max);
delegate void IncrementProgressCallback();
delegate void UpdateLabelCallback(string text);
delegate void CompleteOperationCallback(string message);
delegate void AddTrackToListCallback(IITFileOrCDTrack fileTrack);
private void IncrementProgress()
{
if (this.progressBar1.InvokeRequired)
{
IncrementProgressCallback cb = new IncrementProgressCallback(IncrementProgress);
this.Invoke(cb, new object[] { });
}
else
{
this.progressBar1.PerformStep();
}
}
private void UpdateLabel(string text)
{
if (this.label1.InvokeRequired)
{
UpdateLabelCallback cb = new UpdateLabelCallback(UpdateLabel);
this.Invoke(cb, new object[] { text });
}
else
{
this.label1.Text = text;
}
}
private void CompleteOperation(string message)
{
if (this.label1.InvokeRequired)
{
CompleteOperationCallback cb = new CompleteOperationCallback(CompleteOperation);
this.Invoke(cb, new object[] { message });
}
else
{
this.label1.Text = message;
}
}
private void AddTrackToList(IITFileOrCDTrack fileTrack)
{
if (this.listView1.InvokeRequired)
{
AddTrackToListCallback cb = new AddTrackToListCallback(AddTrackToList);
this.Invoke(cb, new object[] { fileTrack });
}
else
{
this.listView1.Items.Add(new ListViewItem(new string[] { fileTrack.Name, fileTrack.Artist, fileTrack.Location, fileTrack.BitRate.ToString() }));
}
}
private void SetupProgress(int max)
{
if (this.progressBar1.InvokeRequired)
{
SetupProgressCallback cb = new SetupProgressCallback(SetupProgress);
this.Invoke(cb, new object[] { max });
}
else
{
this.progressBar1.Maximum = max;
this.progressBar1.Minimum = 1;
this.progressBar1.Step = 1;
this.progressBar1.Value = 1;
}
}
#endregion
}
}