Migrating song ratings from Apple Music to Strawberry
-
I'm making the switch from Apple Music (Not the streaming service, just the application, to which I have imported several thousand songs over the years from various CDs and such) and I would like to migrate my song ratings from Apple Music to Strawberry. On Apple Music, you can rate each song from 1-5 stars, and you can additionally favorite songs (used to be a heart icon, is now a star icon). If possible, I would like to transfer both of these pieces of information for each of my songs from my Apple Music to my Strawberry. I can't find any information on how to do that, so I was wondering if anyone here could help me.
-
After much trial and error, I created a (very bad) nodejs script to migrate the settings from an exported Library.xml. For anyone who runs across this comment later on, please read the comments carefully before you decide to run this code. It's what worked for my personal situation, and it may have unexpected behavior if your needs differ from mine.
/* * HACKS, do not use for real work, make sure to BACKUP your database beforehand and CHECK if everything went smoothly afterwards. * This script is designed to migrate song ratings from Apple Music / iTunes to Strawberry. It was made for / last tested on MacOS Sonoma. * This is bad code, if someone cleans it up you might want to use that version * Make sure to change the constants (IN_SCREAMING_SNAKE_CASE) to what best suits your needs * Also don't forget to change the logic for converting iTunes ratings to Strawberry ones (Where it says CHANGEME). I've chosen here to decrease every rating by a half star for every song that isn't marked as loved. That's my personal preference, and it allows me to include that extra bit of information into my rating data (because Strawberry, at time of writing, does not seem to have a love function) but please change it if you don't want this behavior. * The dependencies (plist, better-sqlite3, and file-url) are all on npm, use your package manager of choice to install them. Also set "type": "module" in your package.json. * kthxbai - oirnoir */ import plist from 'plist'; import fs from 'fs'; import Database from 'better-sqlite3'; import fileUrl from 'file-url'; // Please, for the love of your own sanity, make a backup of your Strawberry database before tinkering around with this script... const ORIGINAL_MUSIC_FOLDER_PREFIX = "file:///Users/oirnoir/Music/Music/Media.localized/Music"; // The string that is before all relative paths in the xml file const MUSIC_FOLDER_PATH = "/home/oirnoir/Music"; // The path of your music folder const STRAWBERRY_DATABASE_PATH = "/home/oirnoir/.local/share/strawberry/strawberry/strawberry.db"; // The path of your strawberry database (Again, BACK IT UP!) const XML_FILE_PATH = "./Library.xml"; // The path of your Library.xml file, exported from iTunes const VERBOSE = true; // Turn off if you don't want to see the sqlite database actions if (!fs.existsSync(STRAWBERRY_DATABASE_PATH)) throw new Error("Strawberry database path does not exist!"); if (!fs.existsSync(XML_FILE_PATH)) throw new Error("Library XML file path does not exist!"); const xml = fs.readFileSync(XML_FILE_PATH, "utf-8"); const parsed = plist.parse(xml); /** * @type {{path: string, url: string, iTunesRating: 0|20|40|60|80|100, loved: boolean}[]} */ const originalRatings = []; const failedPaths = []; for (const item of Object.values(parsed.Tracks)) { const locationOriginal = item.Location; const location = decodeURIComponent(locationOriginal.replace(ORIGINAL_MUSIC_FOLDER_PREFIX, MUSIC_FOLDER_PATH)).normalize(); // Replace these paths with the paths in your XML file and the paths to your actual music if (!fs.existsSync(location)) { failedPaths.push(location); continue; } originalRatings.push({ path: location, url: fileUrl(location), iTunesRating: item.Rating ?? 0, loved: item.Loved == true // Because it can be undefined and we want to force it to be a boolean }) } if (failedPaths.length > 0) { console.log(failedPaths); throw new Error("The above paths specified in the XML file do not exist on this system!"); } const newRatings = originalRatings.map(r => { let newRating = r.iTunesRating; // CHANGEME: This is how I prefer to translate loved status. Strawberry doesn't have loved status, so I'll settle for decreasing the star count by a half star as a compromise. if (newRating >= 10 && !r.loved) newRating -= 10; // Now we have an integer from 0-100, which is how iTunes stores ratings. For some reason, strawberry chooses to store these as floating point numbers, so we'll divide by 100. return { path: r.path, url: r.url, rating: newRating / 100 } }) const db = new Database(STRAWBERRY_DATABASE_PATH, {fileMustExist: true, verbose: VERBOSE ? console.log : undefined}); const songs = db.prepare(`SELECT url, fingerprint FROM songs`).all(); // Yes this is terrible practice but what the heck /** * @type {{path: string, url: string, rating: number, fingerprint: string}[]} */ const fingerprinted = []; const notFound = []; // RIP your computer // This code should never make it into production, it's hilariously inefficient console.log("Associating fingerprints with songs in strawberry database, this may take a while because this is terrible code..."); for (const rating of newRatings) { const row = songs.find(s => encodeURI(decodeURI(s.url)) == encodeURI(decodeURI(rating.url))); if (!row) notFound.push(rating); fingerprinted.push({ ...rating, fingerprint: row.fingerprint }) } console.log("Done with fingerprint associations"); if (notFound.length > 0) { console.log(notFound); throw new Error("The above paths were not found in the database."); } // It's write time! console.log("Writing changes to database..."); for (const item of fingerprinted) { db.prepare(`UPDATE songs SET rating = ? WHERE fingerprint = ?`).run(item.rating, item.fingerprint); } console.log("Done?")
-
Additionally, note that if you run this script without modifying the logic, note that songs that have not been marked as loved will be lowered by a half-star rating. This is so that songs that were marked as loved will have an even rating, and songs that were not will have an odd one. This is possible because iTunes has 5 possible rating options and Strawberry has 10. If you do not want this behavior, please change it when you run it!
In case you need step-by-step directions to run it (Linux, not sure about mac or windows):
- Download and install node.js
- Make a directory and CD into it
- Save this code into a .js file
- Export your iTunes Library.xml file and copy it into the folder (or somewhere nearby. You'll need to specify its path)
- Install dependencies:
npm i plist better-sqlite3 file-url
- Edit the javascript file! Change all of the constants at the top to those that fit your needs. If those aren't right, it can lead to unexpected behavior.
- As I explained at the top of this post, make sure you like the rating conversion logic or change it if you don't
- Back up your strawberry database
- Run (
node ./script.js
) and hope this doesn't break your library
Feel free to suggest edits / comments / concerns