Skip to content

Instantly share code, notes, and snippets.

@Jericho
Created November 21, 2023 20:38
Show Gist options
  • Save Jericho/7791e7e35917db695f3c395c1e580ff3 to your computer and use it in GitHub Desktop.
Save Jericho/7791e7e35917db695f3c395c1e580ff3 to your computer and use it in GitHub Desktop.
Remove duplicates from my iTunes library and other clean up tasks
// You need to add a reference to the iTunes COM object which is present on your system after you install iTunes
using iTunesLib;
namespace MyiTunesLibraryCleanUp
{
public class Program
{
public static void Main()
{
// This is the location where I store my music.
var rootFolder = @"C:\Users\desau\OneDrive\Music\iTunes\iTunes Media\Music";
// Get all tracks
var tracks = GetAllTracks();
/* ==================================================
* Fix tracks that point to duplicate files
* (meaning files that end with "*1.m4a", "*2.m4a", etc.
* ================================================== */
// Find the tracks that end with a space followed by a digit (i.e.: [0..9]).
// For example: "My Song 1.m4a", "My Other Song 2.mp3"
// TODO: improve this logic to take into consideration the possibility (however slim) that
// the number at the end of the song name is greather than 9. For example: "My song 12.m4a".
var nameEndsWithNumber = tracks
.Where(t => !string.IsNullOrEmpty(t.Location))
.Select(t => (Track: t, FileInfo: new FileInfo(t.Location)))
.Where(trackInfo =>
{
var lastChars = Path.GetFileNameWithoutExtension(trackInfo.FileInfo.Name).TakeLast(2).ToArray();
if (!Char.IsNumber(lastChars[1])) return false;
if (!Char.IsWhiteSpace(lastChars[0])) return false;
return true;
})
.ToArray();
// Replace the track location with the original file. For example: replace "My Song 1.m4a" and "My Song 2.m4a" with "My Song.m4a".
// Since I store my music on OneDrive and I have "Files On-Demand" enabled, the actual music files are typically not on my hard drive.
// The following loop will cause the original file to be sync'ed to my hard drive and therefore it can take quite a while to complete
// depending on the number of tracks that need to be modified.
foreach (var trackInfo in nameEndsWithNumber)
{
var originalFileName = GetOriginalFileName(trackInfo.FileInfo.Name, trackInfo.FileInfo.Directory.FullName);
if (Path.Exists(originalFileName))
{
trackInfo.Track.Location = originalFileName;
Console.WriteLine($"Fixed the file location for this track: {trackInfo.Track.Album} - {trackInfo.Track.Name}");
}
}
/* ==================================================
* Delete duplicate files
* ================================================== */
var atLeastOneTrackDeleted = false;
// Delete the files that end with a number. For example: "My Song 1.m4a", "My Other Song 2.mp3", etc.
var filesEndingWithNumber = Directory.GetFiles(rootFolder, "*.mp3", SearchOption.AllDirectories)
.Union(Directory.GetFiles(rootFolder, "*.m4a", SearchOption.AllDirectories))
.Where(fileName =>
{
var lastChars = Path.GetFileNameWithoutExtension(fileName).TakeLast(2).ToArray();
if (!Char.IsNumber(lastChars[1])) return false;
if (!Char.IsWhiteSpace(lastChars[0])) return false;
return true;
})
.ToArray();
foreach (var fileEndingWithNumber in filesEndingWithNumber)
{
var originalFileName = GetOriginalFileName(fileEndingWithNumber);
if (Path.Exists(originalFileName))
{
File.Delete(fileEndingWithNumber);
Console.WriteLine($"Deleted file: {fileEndingWithNumber}");
atLeastOneTrackDeleted = true;
}
}
if (atLeastOneTrackDeleted) tracks = GetAllTracks();
atLeastOneTrackDeleted = false;
/* ==================================================
* Combine duplicate tracks
* ================================================== */
var duplicatesByLocation = tracks
.GroupBy(t => t.Location)
.Where(group => group.Count() > 1 && !string.IsNullOrEmpty(group.Key))
.ToArray();
foreach (var duplicates in duplicatesByLocation)
{
atLeastOneTrackDeleted |= CombineDuplicateTracks(duplicates);
Console.WriteLine($"Combined duplicates of this track: {duplicates.First().Album} - {duplicates.First().Name}");
}
if (atLeastOneTrackDeleted) tracks = GetAllTracks();
atLeastOneTrackDeleted = false;
var duplicatesBySongName = tracks
.GroupBy(t => (t.Album, t.Name))
.Where(group => group.Count() > 1)
.Where(group => group.Key != ("Human", "Human")) // Rod Steward and Rag'n'Bone Man both have a song called "Human" on an album titled "Human". These two tracks are not duplicates.
.ToArray();
foreach (var duplicates in duplicatesBySongName)
{
atLeastOneTrackDeleted |= CombineDuplicateTracks(duplicates);
Console.WriteLine($"Combined duplicates of this track: {duplicates.First().Album} - {duplicates.First().Name}");
}
if (atLeastOneTrackDeleted) tracks = GetAllTracks();
atLeastOneTrackDeleted = false;
/* ==================================================
* Fix tracks with an un-specified file (or the file was deleted).
* ================================================== */
var tracksUnknownLocation = tracks
.Where(t => string.IsNullOrEmpty(t.Location) || !File.Exists(t.Location))
.ToArray();
foreach (var trackUnknownLocation in tracksUnknownLocation)
{
var safeAlbumName = trackUnknownLocation.Album
.Replace('"', '_')
.Replace('?', '_')
.Replace('/', '_')
.Replace(':', '_');
var safeTrackName = trackUnknownLocation.Name
.Replace('"', '_')
.Replace('?', '_')
.Replace('/', '_')
.Replace(':', '_')
.Substring(0, Math.Min(trackUnknownLocation.Name.Length, 33)) // Evidently, iTunes limits the name of a file to 33 characters
.Trim();
var trackPrefix = trackUnknownLocation.DiscCount > 1 ? $"{trackUnknownLocation.DiscNumber}-" : String.Empty;
var fileName = $"{rootFolder}\\{trackUnknownLocation.AlbumArtist ?? trackUnknownLocation.Artist}\\{safeAlbumName}\\{trackPrefix}{trackUnknownLocation.TrackNumber:00} {safeTrackName}.m4a";
if (File.Exists(fileName))
{
trackUnknownLocation.Location = fileName;
Console.WriteLine($"Fixed the file location for this track: {trackUnknownLocation.Album} - {trackUnknownLocation.Name}");
}
else
{
fileName = Path.ChangeExtension(fileName, "mp3");
if (File.Exists(fileName))
{
trackUnknownLocation.Location = fileName;
Console.WriteLine($"Fixed the file location for this track: {trackUnknownLocation.Album} - {trackUnknownLocation.Name}");
}
else
{
//trackUnknownLocation.Delete();
Console.WriteLine($"Unable to locate the file for this track: {trackUnknownLocation.Album} - {trackUnknownLocation.Name}");
}
}
}
/* ==================================================
* Find files that do not have a corresponding track in the library
* Please note that a given mp3 file could have a corresponding m4a in the libary (and vice-versa).
* I want to ignore these files. Otherwise I would create a duplicate track in the iTunes library for the same song.
* ================================================== */
var tracksLocation = tracks.Select(track => track.Location).Order().ToArray();
var mp3NotInLibrary = Directory.GetFiles(rootFolder, "*.mp3", SearchOption.AllDirectories)
.Where(fileName => !tracksLocation.Contains(fileName, StringComparer.OrdinalIgnoreCase))
.Where(fileName => !tracksLocation.Contains(Path.ChangeExtension(fileName, ".m4a"), StringComparer.OrdinalIgnoreCase))
.ToArray();
var m4aNotInLibrary = Directory.GetFiles(rootFolder, "*.m4a", SearchOption.AllDirectories)
.Where(fileName => !tracksLocation.Contains(fileName, StringComparer.OrdinalIgnoreCase))
.Where(fileName => !tracksLocation.Contains(Path.ChangeExtension(fileName, ".mp3"), StringComparer.OrdinalIgnoreCase))
.ToArray();
var filesNotInLibrary = mp3NotInLibrary.Union(m4aNotInLibrary).ToArray();
iTunesAppClass iTunes = new();
foreach (var fileName in filesNotInLibrary)
{
IITOperationStatus status = iTunes.LibraryPlaylist.AddFile(fileName);
while (status.InProgress)
Thread.Sleep(500);
Console.WriteLine($"Added new track: {status.Tracks[0].Album} - {status.Tracks[0].Name}");
}
Console.WriteLine("Done");
}
private static string GetOriginalFileName(string filePath)
{
var directory = Path.GetDirectoryName(filePath);
var fileName = Path.GetFileName(filePath);
return GetOriginalFileName(fileName, directory);
}
private static string GetOriginalFileName(string fileName, string directory)
{
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
var extension = Path.GetExtension(fileName);
var originalFileName = Path.Combine(directory, fileNameWithoutExtension[..(fileNameWithoutExtension.Length - 1)].Trim() + extension);
return originalFileName;
}
private static bool CombineDuplicateTracks(IEnumerable<IITFileOrCDTrack> duplicates)
{
var returnValue = false;
var duplicateTracks = duplicates.OrderBy(t => t.DateAdded).ToArray();
var originalTrack = duplicateTracks.First();
originalTrack.PlayedCount = duplicates.Sum(t => t.PlayedCount);
foreach (var duplicate in duplicateTracks.Except(new[] { originalTrack }))
{
duplicate.Delete();
returnValue = true;
}
return returnValue;
}
private static IITFileOrCDTrack[] GetAllTracks()
{
// Create a reference to iTunes
iTunesAppClass iTunes = new();
// Get a reference to the collection of all tracks
IITTrackCollection trackCollection = iTunes.LibraryPlaylist.Tracks;
// Cast the tracks
var tracks = trackCollection
.Cast<IITTrack>()
.Where(t => t.Kind == ITTrackKind.ITTrackKindFile)
.Cast<IITFileOrCDTrack>()
.ToArray();
return tracks;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment