Created
November 21, 2023 20:38
-
-
Save Jericho/7791e7e35917db695f3c395c1e580ff3 to your computer and use it in GitHub Desktop.
Remove duplicates from my iTunes library and other clean up tasks
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
// 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