我想从我的Google Play音乐帐户中打印歌曲列表(包括歌手,专辑,评分,以及播放次数和时长)。

没有简单的方法可以从该应用程序。在翻阅一长串歌曲时执行打印屏幕是不可行的。

我很高兴将数据导出为标准格式(纯文本,CSV,XML等),我可以操纵自己。

有什么建议吗?

评论

这使我还创建了一种方法,用于查看哪些歌曲不在Google Play播放列表中,并准确查看歌曲在哪些Google Play播放列表中。

我无法回答,但是PlayListExpo Chrome扩展程序会保存您滚动浏览的播放列表的CSV:chrome.google.com/webstore/detail/playlistexpo-for-google-p / ...

#1 楼

注意:虽然这个答案仍然是完全准确的,但现在Google逐步淘汰Google Play音乐,转而使用YouTube音乐。如果您使用他们的自动转换器,则您的播放列表(包括上传的音乐)将保留在YouTube音乐中。不幸的是,上传的歌曲在共享播放列表中也看不到。因此,我为YouTube音乐制作了以下脚本。

修改darkliquid的答案,我想到了以下内容,该内容可以一次保存多个播放列表。
说明:

转到“播放列表”页面。
将下面的JavaScript代码粘贴到控制台中(按F12键打开控制台)。
单击要保存为文本的播放列表。
在播放列表页面上,相对缓慢地滚动到底部(以便可以看到每个条目)。
滚动到底部后,导航回到播放列表页面(与步骤5中的步骤相同) 1.)使用菜单或浏览器的后退按钮。
对要保存到文本的所有播放列表重复步骤3-5。
对所有要保存到的播放列表完成此操作文本,您可以键入JSON.stringify(tracklistObj, null, '\t')(如果希望最小缩进,则将'\t'更改为' '),或者如果您只希望JavaScript对象以自己的方式操作,则可以输入tracklistObj。如果希望对其进行排序,请在调用Object.values(tracklistObj).forEach(a => a.sort())命令之前运行命令JSON.stringify

请注意在完成所有要完成的操作之前不要刷新页面,否则必须重新启动从第1步开始。 >请注意,您可能会忽略控制台中的所有GET和POST错误(这些错误是由Play音乐本身生成的,而不是此脚本生成的)。
还要注意,当前仅设置printTracksToConsole,但是您可以轻松编辑包含trueArtist - Track nametracklistObj[currentPlaylist].push(artist + " - " + title);albumplayCount的行,和/或任何所需的格式(如果需要,请包括CSV格式)。请在步骤2之前执行此操作。
示例输出(我目前拥有的所有Google Play播放列表)均带有默认设置。导航到32个播放列表中的每一个,向下滚动它们,然后将结果转换为文本,总共花费了大约5分钟。
P.S.您可能有兴趣使用我发现的一个名为Tune My Music的网站从输出中制作YouTube播放列表(但YouTube将播放列表的创建限制为每天10个),以便您的朋友可以收听您的Google播放列表。如果执行此操作,则可能要使用TextMechanic之类的方法从输出列表中删除引号和duration

评论


如果只有比在控制台中粘贴JavaScript更好的方法了。 (自从Ublock Origin阻止脚本以来,我还有些打ic。)但是,这确实满足了我的需要。

–ale
17年6月1日在22:18

恐怕现在已经过时了:( TypeError:无法读取位于c(play-music.gstatic.com/ fe / 6..e / listen__en_gb.js:1190:211)

– FloriOn
18年2月19日在14:51

@FloriOn感谢您的评论!我更新了代码,以便现在可以再次使用。

– Zach Saucier
18年2月19日在16:50

@ale有。您可以将代码转换为书签。

– David Metcalfe
18年5月5日23:34

运行此代码时出现控制台错误,但似乎并未阻止它运行

–奥修斯
18/12/8在9:13



#2 楼

(已更新2016-05-09,比当前的最佳答案更可靠)

如果您只需要保存一些播放列表,则可以使用下面的Javascript代码段。此代码段可以保存网页上显示的每个列表,因此它也适用于所有歌曲/专辑/艺术家库视图。我在此答案的末尾列出了其他两种选择。


转到:https://play.google.com/music/listen#/all(或您的播放列表)
打开开发人员控制台(适用于Chrome的F12)。将下面的
代码粘贴到控制台中。
所有抓取的歌曲都存储在allsongs对象中,并且列表的文本版本已复制到剪贴板。我建议先运行
songsToText("all",true)以获取完整的CSV信息。如果剪贴板的复制在第一次尝试时不起作用,请手动运行copy(outText)

代码(最新版本,2016年5月10日,修订版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);


有关Github(Gist)的最新代码,请访问:https://gist.github.com/jmiserez/c9a9a0f41e867e5ebb75



如果您要在文本格式,可以调用
songsToText()函数。您可以选择一种风格,选择
格式,如果只导出喜欢/翻录过的歌曲,则可以导出。
然后将结果列表粘贴到剪贴板中。
样式是allartistartistalbumartistsong
artistalbumsong
CSV将生成一个CSV文件,可以将其忽略(默认为false)。
仅被选中的likedonly(默认为
false)或设置为true,并将过滤所有歌曲
的评级大于或等于5。
例如:



songsToText("all",true,false)将以csv格式导出所有歌曲。

songsToText("all",true,true)将仅导出csv格式的喜欢的歌曲。

songsToText("artistsong",false,false)将所有歌曲导出为文本。


然后您可以将数据粘贴到任何您喜欢的位置,例如
示例http://www.ivyishere.org/如果要将
歌曲或专辑添加到您的Spotify帐户中。制作常春藤
识别完整专辑,使用“ artistalbum”风格。对于
歌曲,请使用“ artistong”风格。

关于摘要:
这是基于Michael Smith的原始答案,但功能更强大。我做了以下改进:


在播放列表和库上工作。任何遗漏的列都会被忽略,并且顺序会确定,因此它应该可以在Google Music中的几乎所有歌曲列表上使用。
它会在到达底部(检测滚动位置)时或在指定的超时后停止。如果滚动检测代码偏离了几个像素,则可以使用超时来防止无限循环。
它更快(间隔为1ms),但是等待数据准备就绪(直到指定的超时) ,目前为3s)。
在操作过程中和在输出上进行重复数据删除。
收集等级:从未评级“ undefined”,未评级“ 0”(即曾经评级但随后被删除),“ 1”是大拇指朝下,“ 5”是朝上(喜欢)。

除了基本的改进之外,它还可以很好地格式化文本并将其复制到剪贴板。如果需要,还可以通过再次运行songsToText函数来获取CSV数据。

替代方法:


如果需要Python API,请查看非官方的Google Music API项目。
如果您有大量的播放列表,并且想一次性导出所有播放列表,请尝试可以执行此操作的gmusic-scripts播放列表导出器(Python,使用非官方的API项目) 。


评论


嘿,只是代码的后续工作:它只会复制最后30首歌曲,而当我执行songsToText(“ artistsong”)时,它会以分钟:秒为单位输出长度,并在播放列表中输出曲目编号。无论如何,歌曲的详细信息都在所有歌曲中,但其中只有30首(我的播放列表有数百首)

– mkln
2015年2月12日在9:36



没关系,歌曲数量不会固定在30首。但是在另一个有130首歌曲的播放列表中,它只会导出前117首。

– mkln
15年2月12日在9:45

@mkln我已经更新了代码,现在它可以处理库,播放列表以及Google音乐中所有其他歌曲列表。只需运行所有内容,它将把播放列表/库/列表作为文本列表复制到剪贴板。如果您需要包含所有内容(播放次数,时长,评分)的CSV版本,请在此之后运行songsToText(“ all”,true)。

– jmiserez
15年2月13日在1:26

很好,谢谢。我正在尝试编写一个保存所有播放列表的python脚本。您将如何通过javascript单击各种播放列表?函数的开头是否可以有一个播放列表选择器?

– mkln
15年2月13日在20:06

@mkln好吧,这家伙已经做到了:github.com/soulfx/gmusic-playlist如果您仅使用他的Python脚本,这可能是最简单的!老实说,直到现在我还没有看到它,但是如果您需要多个播放列表,那可能是更好的选择。

– jmiserez
2015年2月14日于1:23

#3 楼

如果您不介意在浏览器开发者控制台中运行一些javascript代码,则可以像这样从网页中提取信息(仅在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); 
}


这将在控制台中打印出窗口中大多数当前可见歌曲的列表。您需要向下滚动并重新运行以获得更多内容。目前,我还没有找到一种完整的方式来获取信息,但是这种5分钟的快速破解总比没有好。

评论


这看起来很有希望。我会去的。

–ale
13年10月31日在23:21

非常感谢您的回答。您节省了我几个小时。我所做的是在要复制的播放列表中反复运行您的脚本。将结果粘贴到名为Text Soap的Mac应用程序中。转换成 ”,”。删除重复项并导出为txt。然后将其更改为CSV,去掉不需要的列,然后使用以下命令将其导入Spotify:ivyishere.org总的来说,我花了大约8分钟的时间才明白了,干杯〜

–user51544
13年11月4日在3:55



没问题,乐于帮助。

–深色液体
13年5月5日在21:09

看起来可以解决问题。我最大的问题是播放列表的大小-我要导出的播放列表中有180个。我最大程度地利用了Chrome窗口,然后将其尽可能缩小了一点。如果我能说服Chrome缩放到10%,我会在一个屏幕上全屏显示……在25%的情况下,要花两轮时间再多一点。 (有机会您可以放大JS吗?)

– RobertB
2013年12月19日在21:22

仅供参考,如果您只是一个元素,请使用querySelector(...)而不是querySelectorAll(...)[0]

– ThiefMaster
2014年12月2日在18:38

#4 楼

(使用当时的最高答案)并想要一个完整的解决方案,我创建了以下代码,该代码向下滚动音乐列表并将JSON对象添加到数组中。

知道确切可见的歌曲后,代码会将所有歌曲添加在一起,然后在最后进行重复删除。 (仅在Chrome中经过测试。)要使用:转到您的音乐库,在其中查看完整的歌曲列表,然后运行

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);


当它到达页面底部时,运行此命令可停止滚动,删除重复数组并将JSON复制到剪贴板。

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);


#5 楼

我有一些更短的JavaScript,您可以将其粘贴到控制台中。无需重新运行代码,您只需向下滚动并添加所有可见的相册。然后,您可以将播放列表下载为电子表格。

说明


转到此处:https://play.google.com/music/listen#/ap / auto-playlist-thumbs-up
打开开发人员工具(F12),并将下面的代码粘贴到“控制台”选项卡中
滚动,以便播放列表中的每个专辑至少可见一次
Double-单击页面上的某处下载export-google-play.csv
在Excel中打开export-google-play.csv

代码



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);



输出



GitHub

#6 楼

我对最佳答案的方法做了一些修改。使用Ivy的复制/粘贴方法(http://www.ivyishere.org/ivy),这对我来说效果更好:

步骤1在Chrome中从Google音乐中打开所需的播放列表并将其粘贴到控制台:

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


这将导致整个播放列表而不是一部分的呈现。

步骤2将此脚本粘贴到控制台中:

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);


步骤3转到Ivy,然后转到步骤2,选择“复制/粘贴”选项卡并将控制台输出粘贴到那里。

编辑

亚历克斯·皮德森(Alex Pedersen)建议的更新脚本<​​br />
迭代samurauturetskys的优化方法(我没有足够的声誉来评论他的帖子)。我认为Googleplay样式已更新,因此下面的脚本再次提供了漂亮的输出。

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);


#7 楼

我相信您可以通过Chrome中NetworkDeveloper Tools选项卡手动保存每个播放列表的JSON版本。


转到要下载的播放列表。
在开发人员工具中激活网络记录。
清除所有记录的网络项目。
重新加载包含播放列表的页面。
通过单击Name按名称对网络项目进行排序。
查找具有Nameloaduserplaylist?u=0&format=jsarray&xt=[snip...]的网络项。该项目的完整URL类似于:
https://play.google.com/music/services/loaduserplaylist?u=0&format=jsarray&xt=[snip...]

右键单击该项目(即loaduserplaylist?[snip...])。选择Copy-> Copy Response
将响应(它应该是播放列表的JSON版本)粘贴到您喜欢的文本编辑器中。保存文件。
为每个要保存的播放列表重复播放。

更新#1-2020年5月26日:许多专辑已经从Google Play音乐中消失了。当我查看播放列表时,这些消失的专辑也会从播放列表本身中静默消失。将帐户移至YouTube音乐后,看看专辑是否会重新出现在播放列表中会很有趣。

更新#2-2020年5月26日:在播放列表(多个)页面上,如果您正确单击播放列表,如果Chrome尚未加载该播放列表,则JSON播放列表将是唯一检索到的网络项目。这使得仅捕获播放列表变得非常容易。

#8 楼

只需执行Ctrl + –直到文本很小,然后选择所有文本即可。它像没有脚本和应用程序的魅力一样工作。

#9 楼

我刚遇到这个问题,正在寻找类似的东西。

我猜,您最好的选择是:


安装“播放列表备份”之类的应用程序
使用此应用将Google音乐播放列表导出到文本文件。
使用FileManager应用(例如Ghost Commander)将其重命名为.m3u。
使用具有更多选项(例如MusiXMatch)的另一个应用打开播放列表。


评论


我想你是说这个程序。不好。我碰巧有一个Android设备,但我不是在寻找Android解决方案。此外,我已经尝试过该应用程序,它无法在设备上不在的轨道上导出数据,因此对我来说毫无用处。

–ale
13年10月30日在17:16

作为Web应用程序的Oliver,我们更喜欢不需要本机应用程序的答案。

– Vidar S. Ramdal
18-10-9在13:59