Exporting macOS Podcasts

I love these self righteous musicians who are outraged by Joe Rogan’s podcast because they claim «he’s killing his audience» but had no issue singing about cocaine, heroin and unprotected sex
@Luckytobeme3

If you like to have some podcasts available outside the Apple Podcast App, it’s possible, although Apple does not make it easy.

First, you have to download the episodes you want to save. They are stored in the Library directory. You don’t usually see it, but if you go to the menu bar on top (while you are on Finder), select «Go», then press the option-key and you can select the «Library» folder. Next go to «Group Containers», then (likely) «243LU875E5.groups.com.apple.podcasts» followed by «Library» (again) and «Cache» (the whole path is /Users/NAME/Library/Group\ Containers/243LU875E5.groups.com.apple.podcasts/Library/Cache/ ). The files are available as .mp3s in that directory.

Unfortunately, they have cryptic names like «5E567FEB-2BEB-4602-B1DA-6772126829A3.mp3». Not the best naming scheme. However, the names of the episodes are stored as well, in an sqlite database. It’s in the «Documents» directory of the «243LU875E5.groups.com.apple.podcasts» directory and named «MTLibrary.sqlite».

There are Browser plugins and Apps that can read sqlite databases. However, I opted for R. I’m not going into the details here and if you don’t know R, you might be better off with an app.

Using R and the RSQLite package, you can read in the database and create a table translating the cryptic name into something more useful. Even better, you can also copy-rename it.

The following script (no warranty) does all that, but you have to adapt it to your needs. It also creates an .csv file with the episode duration and a few other infos. In any case, you have to adapt the directories, esp. USERNAME. Oh, and it copies the files to the Documents/Media/Podcasts/ directory.

library(RSQLite)
library(av)
library(lubridate)
#
con <- dbConnect(RSQLite::SQLite(), "/Users/USERNAME/Library/Group\ Containers/243LU875E5.groups.com.apple.podcasts/Documents/MTLibrary.sqlite")
dbListTables(con)
episodes <- dbReadTable(con, "ZMTEPISODE")
podcasts <- dbReadTable(con, "ZMTPODCAST")
dbDisconnect(con)
#
fileDirectory <- "/Users/USERNAME/Library/Group\ Containers/243LU875E5.groups.com.apple.podcasts/Library/Cache/"
fileList <- list.files(fileDirectory)
#
cleanFileName <- function(x) {
x <- str_replace_all(x, ":", " - ")
x <- str_replace_all(x, " ", " ")
x <- str_replace_all(x, "\\.", "")
x <- str_replace_all(x, "\\?", "")
x <- str_replace_all(x, "\\!", "")
x <- str_replace_all(x, "\\/", "-")
return(x)
}
#
podData <- tibble(mp3filename = fileList) %>%
filter((mp3filename != "IMImageStore-Default") & (mp3filename != "JSStoreDataProvider")) %>%
separate(mp3filename, c("fid", "extension"), "\\.", remove = FALSE)
importData <- episodes %>% select(ZAUTHOR, ZITEMDESCRIPTIONWITHOUTHTML, ZTITLE, ZUUID, ZPODCASTUUID, ZPUBDATE)
podData <- podData %>% left_join(importData, by = c("fid" = "ZUUID"))
importData <- podcasts %>% select(ZTITLE, ZUUID)
podData <- podData %>% left_join(importData, by = c("ZPODCASTUUID" = "ZUUID"))
podData <- podData %>% select(-fid, -ZPODCASTUUID) %>%
rename( `authors` = `ZAUTHOR`,
`episodeDescription` = `ZITEMDESCRIPTIONWITHOUTHTML`,
`episodeTitle` = `ZTITLE.x`,
`podcastTitle` = `ZTITLE.y`,
`publicationDate` = `ZPUBDATE`) %>%
mutate(publicationDate = as.character(as.POSIXct(publicationDate, origin = '2001-01-01')),
cleanFileName = paste0(cleanFileName(episodeTitle), ".", extension),
durationText = "",
duration = 0)
#
for(i in 1:nrow(podData)) {
currentFile <- paste0(fileDirectory, podData$mp3filename[[i]])
duration <- round(av_media_info(currentFile)$duration,0)
podData$duration[[i]] <- duration
podData$durationText[[i]] <- str_to_lower(seconds_to_period(duration))
}
#
write_csv(podData, "My Library/data/podcasts.csv")
#
podcastNamesUnique <- unique(podData$podcastTitle)
for(i in 1:length(podcastNamesUnique)) {
if( !dir.exists( paste0("/Users/USERNAME/Documents/Media/Podcasts/", podcastNamesUnique[[i]]) ) ) {
dir.create(paste0("/Users/USERNAME/Documents/Media/Podcasts/", podcastNamesUnique[[i]]))
print(paste0("Created Directory: ", podcastNamesUnique[[i]]))
}
}
#
for(i in 1:nrow(podData)) {
cleanEpisodeTitle <- cleanFileName(podData$episodeTitle[[i]])
print(cleanEpisodeTitle)
file.copy(paste0(fileDirectory, podData$mp3filename[[i]]),
paste0("/Users/USERNAME/Documents/Media/Podcasts/", podData$podcastTitle[[i]], "/", cleanEpisodeTitle, ".", podData$extension[[i]]),
copy.date = TRUE)
}

Have fun.