Automate Playlists with an iTunes Library Parser: Tips & Scripts
Automating playlists using an iTunes Library parser saves time, keeps music organized, and enables dynamic, rule-based collections (e.g., weekly mixes, workout songs, or discovery queues). This article shows practical approaches, parsing tips, and ready-to-run scripts to build automated playlists from your iTunes (Music.app) library.
How iTunes/Music stores library data
- File: iTunes uses an XML file named
iTunes Library.xml(older macOS) or the Music app stores library data in a database and an optional exported XML. - Key fields: Track ID, Name, Artist, Album, Genre, Kind, Location (file path/URL), Play Count, Last Played, Rating, Date Added, BPM, Compilation, Year, Size, Persistent ID.
- Format: XML with nested dictionaries (or exported JSON if you convert it). Many parsers read the XML into native structures.
Choose your parsing approach
- XML parser (recommended): Use an XML library to load and traverse the property-list structure (plist). Reliable for direct reading of
iTunes Library.xml. - Convert to JSON: For languages with stronger JSON support, convert plist → JSON (tools: plutil on macOS or online converters).
- Use a DB API: If you have direct access to the Music app’s database, query it with SQL. This requires careful handling and may break with app updates.
General playlist automation strategy
- Load library into memory (parse XML/plist or JSON).
- Normalize metadata: unify casing, trim whitespace, convert date strings to timestamps, parse numeric fields (BPM, Rating, Play Count).
- Define rules for playlist membership (filters and sorting). E.g., “Top 50 by play count added in last year” or “BPM 120–140, not explicit, rating ≥ 4.”
- Select tracks by applying filters and sort criteria.
- Export playlist as an M3U, XML playlist, or use AppleScript/Apple Music APIs to create playlists in the app.
- Schedule or trigger updates via cron/launchd, shortcuts, or a simple GUI.
Practical tips
- Cache parsed data to speed repeated runs; re-parse only when modification time of the library file changes.
- Respect paths: Local tracks use file:// URLs; URL-decode paths before file operations.
- Handle missing/partial metadata with fallbacks (e.g., treat missing BPM as 0 or exclude).
- Rate-limit app updates: If you create many playlists or update frequently, batch updates to avoid UI/API issues.
- Use stable IDs: Prefer Persistent ID or Track ID to avoid duplicates when regenerating playlists.
- Test on copies of your library files before running destructive actions.
Example scripts
Below are concise examples in Python and Bash to illustrate parsing, filtering, and exporting a playlist.
Python: Parse iTunes XML and export an M3U of top-played tracks
python
#!/usr/bin/env python3 # Requires Python 3.8+. Uses plistlib from stdlib. import plistlib from pathlib import Path from urllib.parse import unquote, urlparse LIB = Path.home() / “Music” / “iTunes” / “iTunes Library.xml” OUT = Path.home() / “Desktop” / “TopPlayed.m3u” with LIB.open(“rb”) as f: data = plistlib.load(f) tracks = data.get(“Tracks”, {}) # Collect tracks with play count entries = [] for tid, t in tracks.items(): pc = t.get(“Play Count”, 0) or 0 loc = t.get(“Location”) if not loc: continue # convert file:// URL to path p = unquote(urlparse(loc).path) entries.append((pc, p, t.get(“Name”))) # top 50 by play count top = sorted(entries, key=lambda x: x[0], reverse=True)[:50] with OUT.open(“w”, encoding=“utf-8”) as f: f.write(”#EXTM3U “) for pc, path, name in top: f.write(f”#EXTINF:{pc},{name} {path} “) print(f”Wrote {len(top)} tracks to {OUT}“)
Bash + xmllint: Create playlist of tracks added in the last 30 days
bash
#!/usr/bin/env bash LIB=“\(HOME</span><span class="token" style="color: rgb(163, 21, 21);">/Music/iTunes/iTunes Library.xml"</span><span> </span><span></span><span class="token assign-left" style="color: rgb(54, 172, 170);">OUT</span><span class="token" style="color: rgb(57, 58, 52);">=</span><span class="token" style="color: rgb(163, 21, 21);">"</span><span class="token environment" style="color: rgb(54, 172, 170);">\)HOME/Desktop/Recent.m3u” THRESHOLD=\((</span><span class="token" style="color: rgb(57, 58, 52);">date</span><span class="token" style="color: rgb(54, 172, 170);"> -v-30d +</span><span class="token" style="color: rgb(163, 21, 21);">"%s"</span><span class="token" style="color: rgb(54, 172, 170);">)</span><span> </span><span class="token" style="color: rgb(0, 128, 0); font-style: italic;"># macOS date; adjust for Linux</span><span> </span> <span></span><span class="token builtin" style="color: rgb(43, 145, 175);">echo</span><span> </span><span class="token" style="color: rgb(163, 21, 21);">"#EXTM3U"</span><span> </span><span class="token" style="color: rgb(57, 58, 52);">></span><span> </span><span class="token" style="color: rgb(163, 21, 21);">"</span><span class="token" style="color: rgb(54, 172, 170);">\)OUT“ # Extract Date Added and Location pairs; crude but effective xmllint –xpath ”//dict/key[text()=‘Tracks’]/following-sibling::dict[1]//dict” “\(LIB</span><span class="token" style="color: rgb(163, 21, 21);">"</span><span> </span><span class="token" style="color: rgb(57, 58, 52);"></span><span> </span><span> </span><span class="token" style="color: rgb(57, 58, 52);">|</span><span> </span><span class="token" style="color: rgb(57, 58, 52);">awk</span><span> </span><span class="token" style="color: rgb(163, 21, 21);">' </span><span class="token" style="color: rgb(163, 21, 21);"> /<key>Date Added/ {getline; gsub(/<|>|/|[a-zA-Z=": -]/,"",\)0); date=\(0} </span><span class="token" style="color: rgb(163, 21, 21);"> /<key>Location/ {getline; gsub(/<|>|/|"/,"",\)0); loc=\(0; print date " " loc} </span><span class="token" style="color: rgb(163, 21, 21);"> '</span><span> </span><span class="token" style="color: rgb(57, 58, 52);"></span><span> </span><span> </span><span class="token" style="color: rgb(57, 58, 52);">|</span><span> </span><span class="token" style="color: rgb(0, 0, 255);">while</span><span> </span><span class="token assign-left environment" style="color: rgb(54, 172, 170);">IFS</span><span class="token" style="color: rgb(57, 58, 52);">=</span><span class="token" style="color: rgb(163, 21, 21);">\)‘ ’ read -r date loc; do ts=\((</span><span class="token" style="color: rgb(57, 58, 52);">date</span><span class="token" style="color: rgb(54, 172, 170);"> -j -f </span><span class="token" style="color: rgb(163, 21, 21);">"%Y-%m-%dT%H:%M:%SZ"</span><span class="token" style="color: rgb(54, 172, 170);"> </span><span class="token" style="color: rgb(163, 21, 21);">"</span><span class="token" style="color: rgb(163, 21, 21);">\)date” +”%s” 2>/dev/null || echo 0) if [ “\(ts</span><span class="token" style="color: rgb(163, 21, 21);">"</span><span> -ge </span><span class="token" style="color: rgb(163, 21, 21);">"</span><span class="token" style="color: rgb(54, 172, 170);">\)THRESHOLD” ]; then # convert file:// path to filesystem path path=\((</span><span class="token" style="color: rgb(54, 172, 170);">python3 -c "from urllib.parse </span><span class="token" style="color: rgb(57, 58, 52);">import</span><span class="token" style="color: rgb(54, 172, 170);"> unquote, urlparse</span><span class="token" style="color: rgb(57, 58, 52);">;</span><span class="token" style="color: rgb(54, 172, 170);"> print</span><span class="token" style="color: rgb(57, 58, 52);">(</span><span class="token" style="color: rgb(54, 172, 170);">unquote</span><span class="token" style="color: rgb(57, 58, 52);">(</span><span class="token" style="color: rgb(54, 172, 170);">urlparse</span><span class="token" style="color: rgb(57, 58, 52);">(</span><span class="token" style="color: rgb(163, 21, 21);">'\)loc’).path))”) echo “\(path</span><span class="token" style="color: rgb(163, 21, 21);">" </span><span class="token" style="color: rgb(163, 21, 21);"> echo "</span><span class="token" style="color: rgb(0, 128, 0); font-style: italic;">#EXTINF:-1,Recent Track" >> </span><span class="token" style="color: rgb(163, 21, 21);">"</span><span class="token" style="color: rgb(54, 172, 170);">\)OUT” echo “\(path</span><span class="token" style="color: rgb(163, 21, 21);">"</span><span> </span><span class="token" style="color: rgb(57, 58, 52);">>></span><span> </span><span class="token" style="color: rgb(163, 21, 21);">"</span><span class="token" style="color: rgb(54, 172, 170);">\)OUT” fi done echo “Recent playlist written to $OUT”
(Adjust date parsing for Linux date differences; the script uses macOS date flags.)
Advanced ideas
- Smart rotation: Keep playlists fresh by rotating out tracks after N plays or when older than M days.
- Crossfade-aware selection: Use track durations and BPM to sequence smoother transitions.
- Machine-learning recommendations: Use embeddings or collaborative filtering on play counts and skips to surface similar but underplayed tracks.
- Integrations: Export playlists to cloud storage, sync with mobile devices, or feed into local MPD servers.
Deployment & scheduling
- macOS: Use launchd (.plist) to run scripts on schedule or on file change.
- Linux: Use cron + inotifywait to trigger on file changes.
- Windows: Use Task Scheduler and a PowerShell script to parse exported XML.
Troubleshooting checklist
- Playlist empty? Check that parser points to the correct library file and that tracks have Location fields.
- Paths incorrect? Ensure URL-decoding and correct handling of spaces/special chars.
- Duplicate entries? Use Persistent ID deduplication.
- App doesn’t show playlist? Export as the Music app’s XML playlist or use AppleScript/Apple Music API to import.
Quick starter rule examples
- Weekly Top 25: Tracks with highest Play Count where Date Added < 7 days ago.
- High-Energy Workout: BPM ≥ 130, Rating ≥ 3, Genre contains “Rock” or “Electronic”.
- Discovery Queue: Rating ≤ 2 and Play Count = 0, sorted by Date Added (newest first).
Closing
Automating playlists with an iTunes Library parser lets you maintain dynamic, personalized collections with little manual effort. Start with small, clearly defined rules, reuse stable IDs for updates, and schedule runs so your playlists stay fresh without intervention.
Leave a Reply