Skip to content

Instantly share code, notes, and snippets.

@geekman
Last active July 11, 2023 13:52
Show Gist options
  • Save geekman/c0690693a85fdb20383edfa12394a880 to your computer and use it in GitHub Desktop.
Save geekman/c0690693a85fdb20383edfa12394a880 to your computer and use it in GitHub Desktop.
WSH script to perform string replacement on iTunes Library media file paths
//
// WSH script to replace all paths in iTunes after moving media files.
// iTunes will prompt you to locate missing files, but you need to do them
// one by one, which is not humanly feasible.
//
// run using: cscript.exe itunes-migrate-files.js
//
// 2022.04.23 darell tan
//
var iTunes = WScript.CreateObject("iTunes.Application");
var wsh = WScript.CreateObject("WScript.Shell");
function hexstr(n) {
var h = '00000000' + (n < 0 ? 0xFFFFFFFF + n + 1 : n).toString(16).toUpperCase();
return h.substr(h.length - 8);
}
function getPersistentID(t) {
var h = iTunes.ITObjectPersistentIDHigh(t),
l = iTunes.ITObjectPersistentIDLow(t);
return hexstr(h) + hexstr(l);
}
function getDictValue(dictElem, key) {
var k = dictElem.selectSingleNode(".//key[.='" + key + "']");
if (k) {
return k.nextSibling.text;
}
return false;
}
function getLibraryFiles(xmlFile) {
var doc = WScript.CreateObject("Msxml2.DOMDocument.6.0");
doc.resolveExternals = false;
doc.setProperty("ProhibitDTD", false);
doc.setProperty("ValidateOnParse", false);
doc.load(xmlFile);
if (doc.parseError.errorCode != 0) {
WScript.Echo("Library XML parse error: " + doc.parseError.reason);
return false;
}
var library = {};
var xmlTracks = doc.selectNodes("//dict[key='Location']");
for (var i = 0; i < xmlTracks.length; i++) {
var t = xmlTracks.item(i);
// remove prefix from Location
var path = getDictValue(t, "Location");
var prefix = "file://localhost/";
if (path.indexOf(prefix) == 0) {
path = path.substr(prefix.length);
// if it starts with slash, likely a UNC path
if (path.substr(0, 1) == '/') path = '/' + path;
path = decodeURIComponent(path);
}
var id = getDictValue(t, "Persistent ID");
if (id.length == 0) {
WScript.Echo("persistent ID not found");
return false;
}
//WScript.Echo("id " + id + " " + path);
library[id] = path;
}
return library;
}
//////////////////////////////////////////////////////////////////////
// you need to export your iTunes Library into an XML first
// because iTunes COM will return an empty path if the file is not found
// it is still in the database however, and can be extracted from the XML file
var lib = getLibraryFiles(wsh.ExpandEnvironmentStrings("%USERPROFILE%\\Music\\iTunes\\Library.xml"));
if (lib === false) {
WScript.Echo("cannot load iTunes Library XML");
WScript.Quit();
}
var tracks = iTunes.LibraryPlaylist.Tracks;
WScript.Echo("total tracks: " + tracks.Count);
// modify the replacement regex and string here
var replaceRE = new RegExp('^//nas-1/', 'i');
var replaceStr = '//nas-2/';
for (var i = 1; i <= tracks.Count; i++) {
var t = tracks.Item(i);
if (t.Kind != 1) continue; // process only file tracks
// skip voice memos
if (t.Genre == "Voice Memo") continue;
var id = getPersistentID(t);
var filePath = t.Location || lib[id];
// if track is not found
if (t.Location == "") {
WScript.Echo(t.Artist + ", " + t.Name + " " + getPersistentID(t) + " " + filePath);
// do the replacement here
var newPath = filePath.replace(replaceRE, replaceStr);
WScript.Echo(newPath);
// this will fail if the path doesn't exist
if (newPath != filePath) {
t.Location = newPath;
}
//break;
}
}
//
// WSH script to remove iTunes tracks with paths that match a regexp.
// These are karaoke/instrumental tracks that should not be played (normally).
//
// run using: cscript.exe itunes-remove-instrumentals.js
//
// 2023.07.11 darell tan
//
var iTunes = WScript.CreateObject("iTunes.Application");
var wsh = WScript.CreateObject("WScript.Shell");
function hexstr(n) {
var h = '00000000' + (n < 0 ? 0xFFFFFFFF + n + 1 : n).toString(16).toUpperCase();
return h.substr(h.length - 8);
}
function getPersistentID(t) {
var h = iTunes.ITObjectPersistentIDHigh(t),
l = iTunes.ITObjectPersistentIDLow(t);
return hexstr(h) + hexstr(l);
}
var tracks = iTunes.LibraryPlaylist.Tracks;
WScript.Echo("total tracks: " + tracks.Count);
// checks for "Instrumental" in path
var pathRE = new RegExp('[/\\\\]instrumental[/\\\\]', 'i');
var matchingTracks = {};
var count = 0;
for (var i = 1; i <= tracks.Count; i++) {
var t = tracks.Item(i);
if (t.Kind != 1) continue; // process only file tracks
// skip voice memos
if (t.Genre == "Voice Memo") continue;
var id = getPersistentID(t);
// if track is not found
if (t.Location != "" && t.Location.match(pathRE)) {
WScript.Echo(t.Artist + ", " + t.Name + " " + getPersistentID(t) + " " + t.Location);
// queue up for deletion later
// note that the track object can't be reliably operated on, so use the ID
matchingTracks[id] = 1;
count++;
}
}
WScript.Echo('found ' + count + ' matching tracks.');
//WScript.Quit(0);
WScript.Echo('deleting now...');
// second sweep to do the deletion
// it's not very convenient to retrieve by some kind of persistent ID
// and multiple rounds are required because the tracks will shift around
while (count > 0) {
for (var i = 1; i <= tracks.Count; i++) {
var t = tracks.Item(i);
if (t.Kind != 1) continue; // process only file tracks
var id = getPersistentID(t);
if (matchingTracks[id] === 1) {
//WScript.Echo(t.Artist + ", " + t.Name + " " + getPersistentID(t) + " " + t.Location);
delete(matchingTracks[id]);
t.Delete();
count--;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment