Skip to content

Instantly share code, notes, and snippets.

@jrtipton
Created April 2, 2012 18:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jrtipton/2286056 to your computer and use it in GitHub Desktop.
Save jrtipton/2286056 to your computer and use it in GitHub Desktop.
iTunes duplicate remover (javascript in Windows)
/*jshint wsh:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:true, devel:true, indent:4, maxerr:50 */
(function () {
"use strict";
//
// Configuration options.
//
// Verbose = print a lot of information
// InformOnly = do not delete, just print what would
// have been deleted
// TrackDurationAllowance = tracks that appear identical
// except for their playtime will be considered the
// same if their playtimes are within this number of
// seconds
// FileSizeAllowance = same as TrackDurationAllowance but
// for the file size (in bytes)
// BitRate128ShouldBeRemovedWhen256IsAvailable = whenever
// there is a track with a bitrate of 128 and there is
// a dupliate with 256, consider them duplicates and
// remove the 128 track.
//
var Options = { Verbose : false,
InformOnly : true,
TrackDurationAllowance : 8,
FileSizeAllowance : 1024,
BitRate128ShouldBeRemovedWhen256IsAvailable : 1 };
//
// Implementation.
//
var ITTrackKindFile = 1;
var iTunesApp = WScript.CreateObject("iTunes.Application");
var separator = "{:separator:}";
function keyForTrack(t) {
return t.Name + separator + t.Artist + separator + t.Album;
}
function addTrackToSet(t, tracksByKey) {
var k = keyForTrack(t);
if (!tracksByKey[k]) {
tracksByKey[k] = [];
}
tracksByKey[k].push(t);
}
function trackDurationInSeconds(t) {
var hms = t.Time,
parts = hms ? hms.split(":") : null,
i = parts ? parts.length - 1 : - 1,
units = 1,
seconds = 0;
while (i >= 0) {
seconds += parts[i] * units;
i--;
//
// This part falls off a cliff if iTunes adds a column for days
// or similar, but for our purposes is still probably fine
//
units *= 60;
}
return seconds;
}
function isWithin(v1, v2, allowance) {
return (v1 >= v2 - allowance &&
v1 <= v2 + allowance);
}
function selectDeleteCandidate(lhs, rhs, tracksToDelete) {
var deleteEntry = { TrackToDelete: null,
TrackToKeep: null };
if (Options.Verbose) {
WScript.Echo("Comparing " + lhs.Name + " {" + lhs.Album + "} to " +
rhs.Name + " {" + rhs.Album + "}...");
}
if (isWithin(trackDurationInSeconds(lhs),
trackDurationInSeconds(rhs),
Options.TrackDurationAllowance)) {
if (Options.Verbose) {
WScript.Echo("They are close enough to the same length.");
}
} else {
if (Options.Verbose) {
WScript.Echo("Their duration isn't close enough (" +
trackDurationInSeconds(lhs) + " vs " +
trackDurationInSeconds(rhs) + ").");
}
return;
}
if (lhs.BitRate === rhs.BitRate) {
if (Options.Verbose) {
WScript.Echo("They have the same bitrate.");
}
} else {
if (Options.Verbose) {
WScript.Echo("Their bitrate differs (" +
lhs.BitRate + " vs " + rhs.BitRate + ").");
}
if (Options.BitRate128ShouldBeRemovedWhen256IsAvailable) {
if ((lhs.BitRate === 128 && rhs.BitRate === 256) ||
(rhs.BitRate === 128 && lhs.BitRate === 256)) {
deleteEntry.TrackToDelete = (rhs.BitRate === 128) ? rhs : lhs;
if (Options.Verbose) {
WScript.Echo("Picking 256 against 128.");
}
}
}
if (!deleteEntry.TrackToDelete) {
return;
}
}
if (deleteEntry.TrackToDelete ||
isWithin(lhs.Size, rhs.Size, Options.FileSizeAllowance)) {
if (Options.Verbose) {
WScript.Echo("Their file sizes are similar.");
}
} else {
if (Options.Verbose) {
WScript.Echo("Their file sizes differ too much.");
return;
}
}
if (!deleteEntry.TrackToDelete &&
lhs.BitRate !== rhs.BitRate) {
deleteEntry.TrackToDelete = (lhs.BitRate > rhs.BitRate) ? lhs : rhs;
}
if (!deleteEntry.TrackToDelete) {
deleteEntry.TrackToDelete = (lhs.PlayedCount > rhs.PlayedCount) ? lhs : rhs;
}
deleteEntry.TrackToKeep = (lhs === deleteEntry.TrackToDelete) ? rhs : lhs;
tracksToDelete.push(deleteEntry);
}
function loadTracks(library) {
//
// Go through all of the tracks in the iTunes library, pick
// a key based on some criteria, and add the track to that
// hash bucket.
//
var tracksByKey = [];
for (var i = library.Tracks.Count; i > 0; i--) {
var t;
t = library.Tracks.Item(i);
if (t.Kind === ITTrackKindFile) {
addTrackToSet(t, tracksByKey);
}
}
return tracksByKey;
}
function selectDeleteCandidates(tracksByKey) {
var tracksToDelete = [];
var i;
var j;
//
// For each hash bucket with at least one collision, look
// at those collisions and try to find a delete candidate.
//
for (var key in tracksByKey) {
if (tracksByKey.hasOwnProperty(key)) {
var set = tracksByKey[key];
if (set.length < 2) {
continue;
}
for (i = 0; i < set.length - 1; i++) {
for (j = i + 1; j < set.length; j++) {
selectDeleteCandidate(set[i], set[j], tracksToDelete);
}
}
}
}
//
// Due to the way we try to pick winners, we might end up
// with the same track scheduled for deletion more than once.
//
for (i = 0; i < tracksToDelete.length; i++) {
for (j = i + 1; j < tracksToDelete.length; j++) {
if (tracksToDelete[i].TrackToDelete === tracksToDelete[j].TrackToDelete) {
tracksToDelete[j].TrackToDelete = null;
}
}
}
return tracksToDelete;
}
function performDeletes(tracksToDelete) {
//
// tracksToDelete is the list of tracks we should, well, delete.
// Go through each one and either delete it or tell the user what
// we would have done.
//
for (var i = 0; i < tracksToDelete.length; i++) {
var t = tracksToDelete[i].TrackToDelete;
if (!t) {
continue;
}
WScript.Echo("Track targeted as unneeded duplicate: " + t.Name + " " + t.Album);
WScript.Echo(" Bitrate: " + t.BitRate);
WScript.Echo(" Size: " + t.Size);
if (Options.InformOnly) {
WScript.Echo("** Not deleting now; Options.InformOnly = true **");
} else {
WScript.Echo("Deleting...");
t.Delete();
}
}
if (Options.InformOnly) {
WScript.Echo("Would have deleted " + tracksToDelete.length + " tracks from the iTunes library.");
} else {
WScript.Echo("Deleted " + tracksToDelete.length + " tracks from the iTunes library.");
}
}
var tracksByKey = loadTracks(iTunesApp.LibraryPlaylist);
var tracksToDelete = selectDeleteCandidates(tracksByKey);
performDeletes(tracksToDelete);
})();
//hi!jrtipton.tumblr.com
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment