41

I want to print a list of songs (with artist, album, rating and, if possible, number of plays and duration) from my Google Play Music account.

There is no easy way to do this from the app. Doing print-screens as I page through a long list of songs is not tenable.

I would be happy with an export of data to a standard format (plain text, CSV, XML, etc.) that I can manipulate myself.

Any suggestions?

17

Modifying darkliquid's answer, I came up with the following which allows for multiple playlists to be saved at once.

Instructions:

  1. Go to Your Playlists page.
  2. Paste in the JavaScript code below into your console.
  3. Click on a playlist that you want to save to text.
  4. Once on the playlist page, scroll to the bottom relatively slowly.
  5. After you've scrolled to the bottom, navigate back to the playlists page (same as in step 1.) using the menu or your browsers back button.
  6. Repeat steps 3-5 for all playlists you want to save to text.
  7. Once you've done this for all the playlists you want to save to text, you can either type JSON.stringify(tracklistObj, null, '\t') (change the '\t' to ' ' if you want minimal indentation) or tracklistObj if you just want the JavaScript object to manipulate it your own way. If you want it sorted, run the command Object.values(tracklistObj).forEach(a => a.sort()) before calling the JSON.stringify command.

Be careful to not refresh the page before you've completed all that you want to do or else you'll have to restart from step 1.

// Setup
var tracklistObj = {},
    currentPlaylist,
    checkIntervalTime = 100,
    lastTime;

// Process the visible tracks
function getVisibleTracks() {
    var playlist = document.querySelectorAll('.song-table tr.song-row');
    for(var i = 0; i < playlist.length ; i++) { 
        var l = playlist[i];

        var title = l.querySelector('td[data-col="title"] .column-content');
        if(title !== null)
            title = title.textContent;

        var artist = l.querySelector('td[data-col="artist"] .column-content');
        if(artist !== null)
            artist = artist.textContent;

        var duration = l.querySelector('td[data-col="duration"] span');
        if(duration !== null)
            duration = duration.textContent;

        var album = l.querySelector('td[data-col="album"] .column-content');
        if(album !== null)
            album = album.textContent;

        var playCount = l.querySelector('td[data-col="play-count"] span');
        if(playCount !== null)
            playCount = playCount.textContent;

        var rating = l.querySelector('td[data-col="rating"]');
        if(rating !== null)
            rating = rating.textContent;

        // Add it if it doesn't exist already
        if(tracklistObj[currentPlaylist] && !tracklistObj[currentPlaylist].includes(artist + " - " + title)) {
            tracklistObj[currentPlaylist].push(artist + " - " + title);

            if(printTracksToConsole) {
                console.log(artist + ' - ' + title);
            }
        }
    }
}

// Listen for page changes
window.onhashchange = function(e) {
    currentPlaylist = null; 

    var doneLoading = setInterval(function() {
        var playListName = document.querySelector('.gpm-detail-page-header h2[slot="title"]');
        if(playListName != null) {
            currentPlaylist = playListName.innerText;
            if(tracklistObj[currentPlaylist] === undefined) {
                tracklistObj[currentPlaylist] = [];
            }

            console.log("===================================");
            console.log("Adding to playlist " + currentPlaylist);

            getVisibleTracks();

            clearInterval(doneLoading);
        }
    }, 100);

}

// Check for new tracks every so often
setInterval(function() {
    getVisibleTracks();
}, checkIntervalTime);

// Whether or not to print the tracks obtained to the console
var printTracksToConsole = false;

You can also print out track names to the console as you go by changing printTracksToConsole to true (you should do this before Step 3).

Note that you can probably ignore all GET and POST errors in the console (these are generated by Play Music itself, not this script).

Also note that currently it's setup only to give Artist - Track name, but you can easily edit the line that has tracklistObj[currentPlaylist].push(artist + " - " + title); with album, playCount, duration, or rating, and/or whatever formatting you want (including CSV format if you so please).

Example output (all Google Play playlists I currently have) with default settings. It took about 5 minutes in total to navigate to each of the 32 playlists, scroll down them, and then convert the result to text.

P.S. You might be interested using a site I found called Tune My Music to make YouTube playlists (but YouTube restricts playlist creation to 10 a day) from the output so your friends can listen to your Google Playlists. If you do this, you probably want to use something like TextMechanic to remove the quotes and .mp3 from the outputted list.

  • 1
    If only there was a better way to do this than pasting JavaScript in the console. (I also had a little hiccup since Ublock Origin blocked the script.) But, this does what I need. – ale Jun 1 '17 at 22:18
  • I'm afraid it is outdated now :( TypeError: Cannot read property 'includes' of undefined at getVisibleTracks (<anonymous>:20:43) at <anonymous>:49:5 at c (play-music.gstatic.com/fe/6..e/listen__en_gb.js:1190:211) – FloriOn Feb 19 '18 at 14:51
  • 4
    @FloriOn Thanks for commenting! I updated the code so that it works again now. – Zach Saucier Feb 19 '18 at 16:50
  • 2
    @ale There is. You can turn the code into a bookmarklet. – David Metcalfe May 5 '18 at 23:34
  • Console errors appear when running this code, but it doesn't seem to block it from running – Otheus Dec 8 '18 at 9:13
31

(Updated 2016-05-09, more robust than current top answer)

If you just need to save a few playlists, you can just use my Javascript snippet below. This snippet can save every list as it is shown on the webpage, so it also works for the all songs/albums/artists library views. I've listed two other alternatives at the end of this answer.

  1. Go to: https://play.google.com/music/listen#/all (or your playlist)

  2. Open a developer console (F12 for Chrome). Paste code below into the console.

  3. All scraped songs are stored in the allsongs object and a text version of the list is copied to the clipboard. I recommend running songsToText("all",true) afterwards to get the full CSV information. Run copy(outText) manually if the clipboard copying didn't work on the first try.

Code (latest version May 10, 2016, Rev 30):

var allsongs = []
var outText = "";
var songsToText = function(style, csv, likedonly){
  if (style === undefined){
    console.log("style is undefined.");
    return;
  }
  var csv = csv || false; // defaults to false
  var likedonly = likedonly || false; // defaults to false
  if (likedonly) {
    console.log("Only selecting liked songs");
  }
  if (style == "all" && !csv){
    console.log("Duration, ratings, and playcount will only be exported with the CSV flag");
  }
  outText = "";
  if (csv) {
    if (style == "all") {
      //extra line
      outText = "artist,album,title,duration,playcount,rating,rating_interpretation" + "\n";
    } else if (style == "artist") {
    } else if (style == "artistsong") {
    } else if (style == "artistalbum") {
    } else if (style == "artistalbumsong") {
    } else {
      console.log("style not defined");
    }
  }
  var numEntries = 0;
  var seen = {};
  for (var i = 0; i < allsongs.length; i++) {
    var curr = "";
    var properTitle = allsongs[i].title.replace(/[\n\r!]/g, '').trim();
    if (!likedonly || (likedonly && allsongs[i].rating >= 5)){
      if (csv) {
        if (style == "all") {
          //extra line
          curr += '"' + allsongs[i].artist.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + allsongs[i].album.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + properTitle.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + allsongs[i].duration.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + allsongs[i].playcount.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + allsongs[i].rating.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + allsongs[i].rating_interpretation.replace(/"/g, '""').trim() + '"';
        } else if (style == "artist") {
          curr += '"' + allsongs[i].artist.replace(/"/g, '""').trim() + '"';
        } else if (style == "artistsong") {
          curr += '"' + allsongs[i].artist.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + properTitle.replace(/"/g, '""').trim() + '"';
        } else if (style == "artistalbum") {
          curr += '"' + allsongs[i].artist.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + allsongs[i].album.replace(/"/g, '""').trim() + '"';
        } else if (style == "artistalbumsong") {
          curr += '"' + allsongs[i].artist.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + allsongs[i].album.replace(/"/g, '""').trim() + '"' + ",";
          curr += '"' + properTitle.replace(/"/g, '""').trim() + '"';
        } else {
          console.log("style not defined");
        }
      } else {
        if (style == "all"){
          curr = allsongs[i].artist + " - " + allsongs[i].album + " - " + properTitle + " [[playcount: " + allsongs[i].playcount + ", rating: " + allsongs[i].rating_interpretation + "]]" ;
        } else if (style == "artist"){
          curr = allsongs[i].artist;
        } else if (style == "artistalbum"){
          curr = allsongs[i].artist + " - " + allsongs[i].album;
        } else if (style == "artistsong"){
          curr = allsongs[i].artist + " - " + properTitle;
        } else if (style == "artistalbumsong"){
          curr = allsongs[i].artist + " - " + allsongs[i].album + " - " + properTitle;
        } else {
          console.log("style not defined");
        }
      }
      if (!seen.hasOwnProperty(curr)){ // hashset
        outText = outText + curr + "\n";
        numEntries++;
        seen[curr] = true;
      } else {
        //console.log("Skipping (duplicate) " + curr);
      }
    }
  }
  console.log("=============================================================");
  console.log(outText);
  console.log("=============================================================");
  try {
    copy(outText);
    console.log("copy(outText) to clipboard succeeded.");
  } catch (e) {
    console.log(e);
    console.log("copy(outText) to clipboard failed, please type copy(outText) on the console or copy the log output above.");
  }
  console.log("Done! " + numEntries + " lines in output. Used " + numEntries + " unique entries out of " + allsongs.length + ".");
};
var scrapeSongs = function(){
  var intervalms = 1; //in ms
  var timeoutms = 3000; //in ms
  var retries = timeoutms / intervalms;
  var total = [];
  var seen = {};
  var topId = "";
  document.querySelector("#mainContainer").scrollTop = 0; //scroll to top
  var interval = setInterval(function(){
    var songs = document.querySelectorAll("table.song-table tbody tr.song-row");
    if (songs.length > 0) {
      // detect order
      var colNames = {
        index: -1,
        title: -1,
        duration: -1,
        artist: -1,
        album: -1,
        playcount: -1,
        rating: -1
        };
      for (var i = 0; i < songs[0].childNodes.length; i++) {
        colNames.index = songs[0].childNodes[i].getAttribute("data-col") == "index" ? i : colNames.index;
        colNames.title = songs[0].childNodes[i].getAttribute("data-col") == "title" ? i : colNames.title;
        colNames.duration = songs[0].childNodes[i].getAttribute("data-col") == "duration" ? i : colNames.duration;
        colNames.artist = songs[0].childNodes[i].getAttribute("data-col") == "artist" ? i : colNames.artist;
        colNames.album = songs[0].childNodes[i].getAttribute("data-col") == "album" ? i : colNames.album;
        colNames.playcount = songs[0].childNodes[i].getAttribute("data-col") == "play-count" ? i : colNames.playcount;
        colNames.rating = songs[0].childNodes[i].getAttribute("data-col") == "rating" ? i : colNames.rating;
      }
      // check if page has updated/scrolled
      var currId = songs[0].getAttribute("data-id");
      if (currId == topId){ // page has not yet changed
        retries--;
        scrollDiv = document.querySelector("#mainContainer");
        isAtBottom = scrollDiv.scrollTop == (scrollDiv.scrollHeight - scrollDiv.offsetHeight)
        if (isAtBottom || retries <= 0) {
          clearInterval(interval); //done
          allsongs = total;
          console.log("Got " + total.length + " songs and stored them in the allsongs variable.");
          console.log("Calling songsToText with style all, csv flag true, likedonly false: songsToText(\"all\", false).");
          songsToText("artistalbumsong", false, false);
        }
      } else {
        retries = timeoutms / intervalms;
        topId = currId;
        // read page
        for (var i = 0; i < songs.length; i++) {
          var curr = {
            dataid: songs[i].getAttribute("data-id"),
            index: (colNames.index != -1 ? songs[i].childNodes[colNames.index].textContent : ""),
            title: (colNames.title != -1 ? songs[i].childNodes[colNames.title].textContent : ""),
            duration: (colNames.duration != -1 ? songs[i].childNodes[colNames.duration].textContent : ""),
            artist: (colNames.artist != -1 ? songs[i].childNodes[colNames.artist].textContent : ""),
            album: (colNames.album != -1 ? songs[i].childNodes[colNames.album].textContent : ""),
            playcount: (colNames.playcount != -1 ? songs[i].childNodes[colNames.playcount].textContent : ""),
            rating: (colNames.rating != -1 ? songs[i].childNodes[colNames.rating].getAttribute("data-rating") : ""),
            rating_interpretation: "",
            }
          if(curr.rating == "undefined") {
            curr.rating_interpretation = "never-rated"
          }
          if(curr.rating == "0") {
            curr.rating_interpretation = "not-rated"
          }
          if(curr.rating == "1") {
            curr.rating_interpretation = "thumbs-down"
          }
          if(curr.rating == "5") {
            curr.rating_interpretation = "thumbs-up"
          }
          if (!seen.hasOwnProperty(curr.dataid)){ // hashset
            total.push(curr);
            seen[curr.dataid] = true;
          }
        }
        songs[songs.length-1].scrollIntoView(true); // go to next page
      }
    }
  }, intervalms);
};
scrapeSongs();
// for the full CSV version you can now call songsToText("all", true);

Latest code on Github (Gist) here: https://gist.github.com/jmiserez/c9a9a0f41e867e5ebb75

  • If you would like the output in a text format, can call the songsToText() function. You can select a style, choose the format, and if only liked/thumbed up songs should be exported. The resulting list will then be pasted into the clipboard. Styles are all, artist, artistalbum, artistsong, artistalbumsong. CSV will result in a CSV file and can be left out (defaults to false). Likedonly can be left out (defaults to false) or set to true, and will filter all songs with ratings greater or equal to 5. E.g:

    • songsToText("all",true,false) will export all songs in csv format.
    • songsToText("all",true,true) will export only liked songs in csv format.
    • songsToText("artistsong",false,false) will export all songs as text.
  • You can then paste the data anywhere you like, for example http://www.ivyishere.org/ if you want to add the songs or albums to your Spotify account. To make Ivy recognize full albums, use the "artistalbum" style. For songs, use the "artistsong" style.

About the snippet: This is based upon Michael Smith's original answer, but is a bit more robust. I have made the following improvements:

  • Works on playlists as well as the library. Any missing columns are ignored and the order is figured out, so it should work on almost any song list inside Google Music.

  • It stops either when it reaches the bottom (detects scroll position), or after the specified timeout. The timeout is there to prevent an endless loop in case the scroll detection code is off by a few pixels.

  • It is much faster (interval every 1ms), but waits if the data is not ready (up to the specified timeout, currently 3s).

  • Does deduplication during operation and on the output.

  • Gathers ratings: "undefined" is never rated, "0" is not rated (i.e. once rated but then removed), "1" is thumbs down, and "5" is thumbs up (liked).

In addition to the basic improvements, it also formats the text nicely and copies it to the clipboard. You can also get the data as CSV if you wish, by running the songsToText function a second time.

Alternatives:

  1. If you need a Python API, check out the unofficial Google Music API project.

  2. If you have tons of playlists and want to export all of them in one go, try the gmusic-scripts playlist exporter that can do that (Python, uses the unofficial API project).

  • Hey just a follow up for the code: it results in only the last 30 songs being copied, and when I do songsToText("artistsong") it outputs the length in minutes:seconds and the track number in the playlist. the songs details are in allsongs anyway, but there are only 30 of them (i have playlists with hundreds) – mkln Feb 12 '15 at 9:36
  • nevermind on the number of songs, it's not stuck at 30. But on another playlist with 130 songs it only exports the first 117. – mkln Feb 12 '15 at 9:45
  • @mkln I've updated the code, now it handles the library, playlists as well as every other list of songs in Google Music. Just run everything and it will copy the playlist/library/list as a text list to the clipboard. If you need a CSV version that includes everything (play count, duration, rating), run songsToText("all", true) afterwards. – jmiserez Feb 13 '15 at 1:26
  • it works great, thanks. I'm trying to write a python script that saves all playlists. how would you click on the various playlists through javascript? would it be possible to have a playlist selector at the beginning of the function? – mkln Feb 13 '15 at 20:06
  • 1
    @mkln Well this guy has already done that: github.com/soulfx/gmusic-playlist Probably easiest if you just use his Python script! I honestly did not see this until now, but it's probably the better option if you need more than one playlist. – jmiserez Feb 14 '15 at 1:23
18

If you don't mind running a bit of javascript code in your browsers developer console, you can extract information from the page like so (only tested in Chrome):

var playlist = document.querySelectorAll('.song-table tr.song-row');
for(var i =0; i<playlist.length ; i++) { 
  var l = playlist[i]; 
  var title = l.querySelector('td[data-col="title"] .column-content').textContent;
  var artist = l.querySelector('td[data-col="artist"] .column-content').textContent;
  var album = l.querySelector('td[data-col="album"] .column-content').textContent;
  console.log(artist + ' --- ' + title + ' --- ' + album); 
}

This will print out to the console a list of most of the currently visible songs in the window. You'll need to scroll down and re-run it to get more. At the moment I haven't figured out a decent way of grabbing the info in it's entirety yet, but this quick 5 minute hack is better than nothing.

  • This looks promising. I'll give it a go. – ale Oct 31 '13 at 23:21
  • 2
    Thank you SO much for this answer. You saved me hours and hours of time. What I did is run your script again and again in the playlist I wanted copied. Paste the results into a Mac app called Text Soap. Turned --- into ",". Removed duplicates and exported out as a txt. Then changed it to CSV, stripped out the unneded columns and imported it to Spotify using: ivyishere.org All in all took me about 8 minutes one I got the hang of it, cheers~ – user51544 Nov 4 '13 at 3:55
  • No problem, happy to help. – darkliquid Nov 5 '13 at 21:09
  • This looks like it's going to do the trick. My biggest problem is the size of my playlists - 180 on the one I'm trying to export. I got around that a bit by maximizing my Chrome window and then zooming out as far as I could. If I could just convince Chrome to zoom to 10% I'd have it all on one screen... at 25%, it took two rounds plus a little bit more. (Any chance you can zoom from JS?) – RobertB Dec 19 '13 at 21:22
  • 1
    FYI, if you just one element, use querySelector(...) instead of querySelectorAll(...)[0] – ThiefMaster Dec 2 '14 at 18:38
3

Using the top answer (at the time) and wanting a complete solution, I've created the following code which scrolls down the music list and adds JSON objects to an array as it goes.

Due to not knowing exactly what songs are visible, the code adds all of them, then de-duplicates at the end. (Only tested in Chrome.)

To use: go to your library, where you see your full song list, and run

var total = [];
var interval = setInterval(function(){
    var songs = document.querySelectorAll("table.song-table tbody tr.song-row");
    for (var i = 0; i < songs.length; i++) {
        total.push({name: songs[i].childNodes[0].textContent,
        length: songs[i].childNodes[1].textContent,
        artist: songs[i].childNodes[2].textContent,
        album: songs[i].childNodes[3].textContent,
        plays: songs[i].childNodes[4].textContent
        });
        songs[i].scrollIntoView(true);
    }
}, 800);

When that gets to the bottom of the page, run this to stop the scrolling, de-duplicate array, and copy JSON to clipboard.

clearInterval(interval);
for (var i = 0; i < total.length; i++) {
    for (var j = i + 1; j < total.length; j++) {
        if (total.hasOwnProperty(i) && total.hasOwnProperty(j) && total[i].name == total[j].name && total[j].artist == total[i].artist) {
            total.splice(j,1);
        }
    }
}
copy(total);
2

I modified the top answer's approach a little bit. This worked better for me with Ivy's copy/paste method (http://www.ivyishere.org/ivy):

Step 1 Open the playlist you want from Google Music in Chrome and paste this into the console:

document.querySelector('body.material').style.height = (document.querySelector('table.song-table tbody').getAttribute('data-count') * 100) + 'px';

This should cause your entire playlist to render rather than only a portion.

Step 2 Paste this script into the console:

var i, j, playlistString = '', playlist = document.querySelectorAll('.song-table tr.song-row');
for (i = 0, j = playlist.length; i < j; i++) {
    var track = playlist[i]; 
    var artist = track.querySelector('[href][aria-label]').textContent;
    var title = track.querySelector('td[data-col="title"]').textContent;
    playlistString += ('"' + artist + '", "' + title + '"\n');
}
console.log(playlistString);

Step 3 Go to Ivy and when you get to step 2 there, select the Copy/Paste tab and paste the console output there.

EDIT

Updated script suggested by Alex Pedersen

Iterating on samurauturetskys refinement (I dont have reputation enough yet to comment on his post). I think Googleplay styling has updated so the script below again gives a pretty output.

var i, j, playlistString = '', playlist = document.querySelectorAll('.song-table tr.song-row');
for (i = 0, j = playlist.length; i < j; i++) {
    var track = playlist[i]; 
    var artist = track.querySelector('[href][aria-label]').textContent;
    var title = track.querySelector('span[class="column-content fade-out tooltip"]').textContent;
    playlistString += ('"' + artist + '", "' + title + '"\n');
}
console.log(playlistString);
2

I have some much shorter JavaScript you can paste into the console. Instead of re-running the code you can just scroll down and all albums that come into view are added. Then you can download the playlist as a spreadsheet.

Instructions

  1. Go here: https://play.google.com/music/listen#/ap/auto-playlist-thumbs-up

  2. Open Developer Tools (F12) and paste the code below into the Console tab

  3. Scroll around so each album in the playlist is visible at least once

  4. Double-click somewhere on the page to download export-google-play.csv

  5. Open export-google-play.csv in Excel.

Code

alert("Please scroll through the playlist so that each album is visible once.\n" + 
      "Then double-click the page to export a spreadsheet.");
var albums = ["Artist,Album,Purchased"];

var addVisibleAlbums = function(){
    [].forEach.call(document.querySelectorAll(".song-row"), function(e){ 
        var albumNodes = [e.querySelector("td[data-col='artist']"), 
              e.querySelector("td[data-col='album']"),
              e.querySelector("td[data-col='title'] .title-right-items")];

        var albumString = albumNodes.map(function(s){ 
            return s.innerText.trim().replace(/,/g,""); 
        }).join(",");

        if(albums.indexOf(albumString) === -1){
            albums.push(albumString); console.log("Added: " + albumString)
        }
    });
}

var createCsv = function(){
    var csv = "data:text/csv;charset=utf-8,";
    albums.forEach(function(row){ csv += row + "\n"; }); 

    var uri = encodeURI(csv);
    var link = document.createElement("a");
    link.setAttribute("href", uri);
    link.setAttribute("download", "export-google-play.csv");
    document.body.appendChild(link);
    link.click(); 
    alert("Download beginning!")
}

document.body.addEventListener("DOMNodeInserted", addVisibleAlbums, false);
document.body.addEventListener("dblclick", createCsv, false);

Output

enter image description here

GitHub

-1

Simply do Ctrl+ till the text is very tiny and then select all of it. It works like a charm without scripts and apps.

-2

I just came across this question looking for something similar.

I guess, your best option is to:

  1. install an app like "Playlist Backup"
  2. Export the Google Music Playlist to a textfile with this app.
  3. Rename it to .m3u with a FileManager app (like Ghost Commander)
  4. Open the playlist with another app that has more options (like MusiXMatch).
  • 1
    I assume you mean this app. No good. While I happen to have an Android device I'm not looking for an Android solution. Further, I've tried this app and it can't export data on tracks that aren't on the device, so it's useless to me. – ale Oct 30 '13 at 17:16
  • 1
    Oliver, being Web Applications, we prefer answers that do not require native apps. – Vidar S. Ramdal Oct 9 '18 at 13:59

protected by Community Dec 11 '15 at 9:35

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.