Skip to content

Instantly share code, notes, and snippets.

@zats
Last active March 11, 2026 17:17
Show Gist options
  • Select an option

  • Save zats/b1e6c4829962c9b0fbda783cd26b806d to your computer and use it in GitHub Desktop.

Select an option

Save zats/b1e6c4829962c9b0fbda783cd26b806d to your computer and use it in GitHub Desktop.
Export OPML from Apple Podcasts app on macOS

Export OPML from Apple Podcasts app on macOS (tested on macOS 12 Monterey)

  1. Download following script
  2. Make downloaded file executable $ chmod +x ~/Download/ApplePodcastToOMPL.js
  3. Run $ ~/Downloads/ApplePodcastToOMPL.js to see the OPML output
  4. If it looks okay, $ ~/Downloads/ApplePodcastToOMPL.js > ~/Downloads/ApplePodcasts.opml

If default sqlite path not working for you, you can specify a custom one $ ~/Downloads/ApplePodcastToOMPL.js /my/custom/MTLibrary.sqlite

#!/usr/bin/env node
const { exit } = require('process');
const PREDEFINED_PATH = "~/Library/Group Containers/243LU875E5.groups.com.apple.podcasts/Documents/MTLibrary.sqlite";
const XML_TEMPLATE = `<?xml version="1.0"?>
<!-- example OPML file -->
<opml version="1.0">
<head>
<title>Overcast Podcast Subscriptions</title>
</head>
<body>
%BODY%
</body>
</opml>`;
const XML_PODCAST_TEMPLATE = `<outline type="rss" title="%TITLE%" text="%TEXT%" xmlUrl="%RSS_URL%" htmlUrl="%WEB_URL%"/>`;
const escapeString = (str) => {
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
};
const args = process.argv.slice(2);
const sqlitePath = (args.length >= 1 ? args[0] : PREDEFINED_PATH).replace(/ /g, '\\ ');
console.log(`sqlitePath: "${sqlitePath}"`);
let jsonOutputString;
try {
jsonOutputString = require('child_process').execSync(`sqlite3 ${sqlitePath} "SELECT ZTITLE,ZITEMDESCRIPTION,ZFEEDURL,ZWEBPAGEURL FROM ZMTPODCAST WHERE ZSUBSCRIBED = 1" -json`).toString();
} catch (error) {
console.error(`Failed to parse sqlite file: ${sqlitePath}`);
exit(-1);
}
const jsonOutput = JSON.parse(jsonOutputString);
const bodyArray = jsonOutput.map((podcast) => XML_PODCAST_TEMPLATE
.replace('%TITLE%', escapeString(podcast['ZTITLE']))
.replace('%TEXT%', escapeString(podcast['ZITEMDESCRIPTION']))
.replace('%RSS_URL%', podcast['ZFEEDURL'])
.replace('%WEB_URL%', podcast['ZWEBPAGEURL']));
const output = XML_TEMPLATE.replace('%BODY%', bodyArray.join('\n'));
console.log(output);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment