Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Reconstruct your playlists from a Google Play Music Takeout export
#!/usr/bin/env python3
r"""
playlist-reconstructor.py will rebuild your Google Play Music (GPM) playlists
into a JSON file from a Google Takeout export.
To use this script:
1. Export your Google Play Music from https://takeout.google.com/settings/takeout
Yes, you have to download the whole thing. Google puts each playlist track into
its own individual .csv file and these are spread through the whole download
so you can't even shortcut and just download the first or last one.
2. Extract each zip file from your Takeout using `unzip`.
This will extract the ZIP file contents into a single Takeouts directory.
3. Run this script and point it to the /Takeout/Google Play Music/Playlists directory:
python3 playlist-reconstructor.py \
--base-dir ~/Downloads/Takeout/Google\ Play\ Music/Playlists \
--output-file ~/Downloads/playlists.json
The output format will look like:
[
{
"Deleted": "",
"Description": "The absolute best decade for music.",
"Owner": "Casey Peel",
"Shared": "",
"Title": "'80s Music",
"Tracks": [
{
"Album": "Tiffany",
"Artist": "Tiffany",
"Duration (ms)": "228856",
"Play Count": "44",
"Playlist Index": "0",
"Rating": "0",
"Removed": "",
"Title": "I Think We're Alone Now"
},
...
},
...
]
"""
import argparse
import csv
import html
import json
import logging
import os
import sys
class PlaylistParseException(Exception):
"""Exception raised upon playlist parse error"""
def reconstruct_playlist(playlist_dir):
"""
Given a playlist directory, reconstruct the playlist and return a dict.
"""
# Attempt to load the playlist metadata if available
metadata_path = os.path.join(playlist_dir, "Metadata.csv")
if os.path.isfile(metadata_path):
with open(metadata_path) as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
playlist = dict(row)
else:
playlist = {"Title": os.path.basename(playlist_dir)}
# Unescape HTML values
playlist = unescape_values(playlist)
# The "Thumbs Up" playlist doesn't have a Tracks subdirectory
if playlist["Title"] == "Thumbs Up":
tracks_dir = playlist_dir
else:
tracks_dir = os.path.join(playlist_dir, "Tracks")
if not os.path.isdir(tracks_dir):
raise PlaylistParseException(
f"Unable to find tracks for {playlist_dir}"
)
tracks = []
for filename in os.listdir(tracks_dir):
if not filename.endswith(".csv"):
continue
with open(os.path.join(tracks_dir, filename)) as csvfile:
reader = csv.DictReader(csvfile)
for track in reader:
tracks.append(unescape_values(track))
# sort the playlist by the track number
tracks.sort(key=lambda info: int(info["Playlist Index"]))
playlist["Tracks"] = tracks
return playlist
def unescape_values(dictionary):
"""
Unescape each value of the dictionary.
"""
return_dict = {}
for key, value in dictionary.items():
return_dict[key] = html.unescape(value)
return return_dict
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--base-dir",
type=str,
help="Directory containing multiple playlist directories",
)
parser.add_argument(
"--playlist-dir",
nargs="?",
type=str,
help="Individual playlist directory",
)
parser.add_argument(
"--output-file",
type=str,
required=True,
help="Filename of the output JSON file",
)
args = parser.parse_args()
if not args.base_dir and not args.playlist_dir:
logging.error("One of --base-dir or --playlist-dir required")
sys.exit(1)
# populate the list of playlist directories
playlist_dirs = []
if args.playlist_dir:
playlist_dirs.append(args.playlist_dir)
if args.base_dir:
for entry in os.listdir(args.base_dir):
path = os.path.join(args.base_dir, entry)
if os.path.isdir(path):
playlist_dirs.append(path)
playlists = []
for playlist_dir in playlist_dirs:
try:
playlist = reconstruct_playlist(playlist_dir)
except PlaylistParseException as exception:
logging.error(exception)
playlists.append(playlist)
# sort playlists by name
playlists.sort(key=lambda playlist: playlist["Title"])
with open(args.output_file, "w") as outfile:
json.dump(playlists, outfile, sort_keys=True, indent=4)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment