Created
April 2, 2012 18:24
-
-
Save jrtipton/2286056 to your computer and use it in GitHub Desktop.
iTunes duplicate remover (javascript in Windows)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*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