Stig's Last.fm Album Linkr

Adding album links and headers to tracks on Last.Fm's recent plays listings - plus linkifying About Me section on profiles

  1. // ==UserScript==
  2. // @name Stig's Last.fm Album Linkr
  3. // @namespace dk.rockland.userscript.lastfm.linkr
  4. // @description Adding album links and headers to tracks on Last.Fm's recent plays listings - plus linkifying About Me section on profiles
  5. // @version 2024.12.02.0
  6. // @author Stig Nygaard, https://www.rockland.dk
  7. // @homepageURL https://www.rockland.dk/userscript/lastfm/linkr/
  8. // @supportURL https://www.rockland.dk/userscript/lastfm/linkr/
  9. // @match *://*.last.fm/*
  10. // @match *://*.lastfm.de/*
  11. // @match *://*.lastfm.es/*
  12. // @match *://*.lastfm.fr/*
  13. // @match *://*.lastfm.it/*
  14. // @match *://*.lastfm.ja/*
  15. // @match *://*.lastfm.pl/*
  16. // @match *://*.lastfm.pt/*
  17. // @match *://*.lastfm.ru/*
  18. // @match *://*.lastfm.sv/*
  19. // @match *://*.lastfm.tr/*
  20. // @match *://*.lastfm.zh/*
  21. // @grant GM.registerMenuCommand
  22. // @grant GM_registerMenuCommand
  23. // @grant GM_getValue
  24. // @grant GM_setValue
  25. // @run-at document-start
  26. // @require https://update.gf.qytechs.cn/scripts/34527/751210/GMCommonAPIjs.js
  27. // @noframes
  28. // ==/UserScript==
  29.  
  30. /*
  31. * Stig's Last.fm Album Linkr is an userscript especially minded album-listeners on Last.Fm.
  32. * It gives you better profile-pages with extra focus on albums in "recent tracks" lists.
  33. *
  34. * https://gf.qytechs.cn/scripts/21153-stig-s-last-fm-album-linkr
  35. * https://github.com/StigNygaard/Stigs_Last.fm_Album_Linkr
  36. * https://www.last.fm/user/rockland
  37. *
  38. * PS. If you like having album-headers on your scrobbles, you might also like my website widget:
  39. * https://lastfm-widgets.deno.dev/
  40. * https://github.com/StigNygaard/lastfm-widgets
  41. */
  42.  
  43. var linkr = linkr || {
  44. // CHANGELOG - The most important updates/versions:
  45. changelog: [
  46. {version: '2024.12.02.0', description: "Improve splitting album titles (detect title-extensions like 'Special Edition' etc)"},
  47. {version: '2021.03.24.0', description: "Certificate error embedding from www.tapmusic.net (album collage), but changing to tapmusic.net seems to fix it."},
  48. {version: '2021.01.29.0', description: "Support the native GM.registerMenuCommand command introduced in GM4.11."},
  49. {version: '2020.12.05.0', description: "Stop using Google Image cache/proxy/resizer. It should not be necessary anymore."},
  50. {version: '2019.10.19.0', description: "Fix for an error happening when live scrobbling a track without cover art."},
  51. {version: '2019.07.08.0', description: "Adapting to last.fm's new scrobble list design and implementation."},
  52. {version: '2019.04.26.0', description: "Probably/hopefully fixing that tapmusic collage could delay loading of some other pageelements?"},
  53. {version: '2019.03.01.1', description: "Remove extra (mobile ad?) line bubbling up in scrobbles list."},
  54. {version: '2018.01.06.0', description: "Making the Linkify-feature optional."},
  55. {version: '2017.10.26.1', description: "Now fully compatible with the upcoming Greasemonkey 4 WebExtension (Use webpage context-menu for options in GM4/Firefox)."},
  56. {version: '2017.08.07.0', description: "Separate links for short and long album titles ('Special Edition', 'Remastered' etc.)"},
  57. {version: '2017.08.01.1', description: "Moving development source to a GitHub repository: https://github.com/StigNygaard/Stigs_Last.fm_Album_Linkr"},
  58. {version: '2017.03.01.0', description: "Found a work-around to keep tapmusic collages working on secure https last.fm pages (https://carlo.zottmann.org/posts/2013/04/14/google-image-resizer.html)."},
  59. {version: '2016.11.05.3', description: "Another bonus-feature added: Optionally embed album collage from http://www.tapmusic.net/lastfm on user's profiles (Enable it via menu in the userscript browser extension)."},
  60. {version: '2016.10.26.0', description: 'More intelligent creation of links in album-headers when there are featured artists on some albumtracks.'},
  61. {version: '2016.10.19.0', description: 'Bonus-feature added: Linkifying URLs written in About Me section in Profiles.'},
  62. {version: '2016.07.04.0', description: '1st release.'}
  63. ],
  64. INFO: true,
  65. DEBUG: false,
  66. observed: null,
  67. linking_running: false,
  68. collagetype: '',
  69. collapseTop: false,
  70. linkifyEnabled: true,
  71. log: function(s, info) {
  72. if ((info && window.console) || (linkr.DEBUG && window.console)) {
  73. window.console.log('*Linkr* '+s);
  74. }
  75. },
  76. albumIcon64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHt2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDIgNzkuYTZhNjM5NiwgMjAyNC8wMy8xMi0wNzo0ODoyMyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIiB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNS41IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMTYtMDctMDdUMTQ6MzQ6MTgrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDI0LTEwLTA1VDA5OjIyOjU0KzAyOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDI0LTEwLTA1VDA5OjIyOjU0KzAyOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpkZGZjMWJlMi00NWMwLTkyNDktODhjNC03MmY0MWViNDg4OTIiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDphOWEwN2UwMy1iOTAyLTg5NGEtOWE4Yi0wOTFmMDBhYmMyNjQiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo4ZDZlYmEyZS03MDU5LWVjNGQtYTIzYi0xZTViZGViNDYwY2MiIHRpZmY6T3JpZW50YXRpb249IjEiIHRpZmY6WFJlc29sdXRpb249IjcyMDAwMC8xMDAwMCIgdGlmZjpZUmVzb2x1dGlvbj0iNzIwMDAwLzEwMDAwIiB0aWZmOlJlc29sdXRpb25Vbml0PSIyIiBleGlmOkNvbG9yU3BhY2U9IjY1NTM1IiBleGlmOlBpeGVsWERpbWVuc2lvbj0iMjQ0IiBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjQ0Ij4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo4ZDZlYmEyZS03MDU5LWVjNGQtYTIzYi0xZTViZGViNDYwY2MiIHN0RXZ0OndoZW49IjIwMTYtMDctMDdUMTQ6MzQ6MTgrMDI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1LjUgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpkNjQ4MWJjZS0zZDgxLTk0NDQtODNiOC1mNDUwNTJlZGVmMzMiIHN0RXZ0OndoZW49IjIwMTYtMDctMDdUMTY6NTE6MTYrMDI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1LjUgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpkZGZj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',
  77. insertStyle: function() {
  78. if (!document.getElementById('linkrStyle')) {
  79. let style = document.createElement('style');
  80. style.id = 'linkrStyle';
  81. style.innerHTML =
  82. `
  83. #tapmusic { font-style:italic; font-size:12px; color:rgb(153,153,153) }
  84. .tapcollage { line-height:1.5; animation:fadein 15s; animation-timing-function:ease-in; }
  85. .tapcredit { line-height:1.3 }
  86. @keyframes fadein { from{color:rgba(153,153,153,0);} to{color:rgba(153,153,153,1);} }
  87. tr.albumlink-row, tr.albumlink-row > td { background-color:#fbe9e9 !important }
  88. tr.albumlink-row > td.chartlist-name { font-style:italic }
  89. tr.albumlink-row > td.chartlist-name > span > span { font-style:normal }
  90. tr.albumlink-row:hover, tr.albumlink-row:hover > td { background-color:#fadcdc !important; }
  91. .albumicon { width:32px; height:32px; margin:0; padding:0; display: block; background-image: url(${linkr.albumIcon64}); background-repeat: no-repeat; background-position: center; background-size: 24px 24px }
  92. .albumextension, .albumextension .link-block-target { font-style:italic; color:#707070 !important }
  93. tr.chartlist-row--interlist-ad.open-ad-container-mobile { display:none !important }
  94. ${ linkr.collapseTop ? 'div[id^="gpt-slot-"], #leader_top { display:none }' : '' }
  95. `;
  96. document.getElementsByTagName('head')[0].appendChild(style);
  97. linkr.log('linkrStyle has been ADDED');
  98. } else {
  99. // linkr.log('linkrStyle was already present');
  100. }
  101. },
  102. loadSettings: function() {
  103. linkr.collagetype = (String(GMC.getValue('collagetype', ''))); // tapmusic collage
  104. linkr.collapseTop = (String(GMC.getValue('collapseTop', 'false'))==='true');
  105. linkr.linkifyEnabled = (String(GMC.getValue('linkifyEnabled', 'true'))==='true');
  106. },
  107. saveSettings: function() {
  108. GMC.setValue('collagetype', String(linkr.collagetype));
  109. GMC.setValue('collapseTop', String(linkr.collapseTop));
  110. GMC.setValue('linkifyEnabled', String(linkr.linkifyEnabled));
  111. location.reload(true);
  112. },
  113. collageOff: function() {
  114. linkr.collagetype = '';
  115. linkr.saveSettings();
  116. },
  117. collage7day: function() {
  118. linkr.collagetype = '7day';
  119. linkr.saveSettings();
  120. },
  121. collage1month: function() {
  122. linkr.collagetype = '1month';
  123. linkr.saveSettings();
  124. },
  125. collage3month: function() {
  126. linkr.collagetype = '3month';
  127. linkr.saveSettings();
  128. },
  129. collage6month: function() {
  130. linkr.collagetype = '6month';
  131. linkr.saveSettings();
  132. },
  133. collage12month: function() {
  134. linkr.collagetype = '12month';
  135. linkr.saveSettings();
  136. },
  137. collageOverall: function() {
  138. linkr.collagetype = 'overall';
  139. linkr.saveSettings();
  140. },
  141. toggleCollapseTop: function() {
  142. linkr.collapseTop = !linkr.collapseTop;
  143. linkr.saveSettings();
  144. },
  145. toggleLinkifyEnabled: function() {
  146. linkr.linkifyEnabled = !linkr.linkifyEnabled;
  147. linkr.saveSettings();
  148. },
  149. linking: function (mutations) {
  150. linkr.log('Entering linking function...', linkr.INFO);
  151. if(linkr.linking_running) {
  152. linkr.log('EXIT linking function, because already running!...', linkr.INFO);
  153. return;
  154. }
  155. linkr.linking_running = true;
  156. function altvalue(elem) {
  157. if (elem?.firstElementChild) {
  158. let albumImg = elem.querySelector('td.chartlist-image > a.cover-art > img');
  159. if (elem.classList.contains('albumlink-row') || elem.firstElementChild.classList.contains('albumlink-row')) {
  160. return null;
  161. } else if (albumImg?.alt) {
  162. return albumImg.alt;
  163. }
  164. }
  165. return null;
  166. }
  167. function containing(s, sub) {
  168. s = s.trim().replace(/^the\s/gi, "").replace(/,\sthe$/gi,"").replace(" & ", " and ").trim();
  169. sub = sub.trim().replace(/^the\s/gi, "").replace(/,\sthe$/gi,"").replace(" & ", " and ").trim();
  170. return (s.toLocaleUpperCase().includes(sub.toLocaleUpperCase()));
  171. }
  172. function splitAlbumTitle(title) {
  173. title = title.trim();
  174. let rtval = {full:title, basic:title};
  175. let regs = [
  176. /^(.+[^-\s])(\s+-\s+)(\w[\w\s]+\sEdition[\w\s]*)$/i,
  177. /^(.+[^-\s])(\s+-\s+)(\w[\w\s]+\sVersion[\w\s]*)$/i,
  178. /^(.+[^-\s])(\s+-\s+)(\w[\w\s]+\sDeluxe[\w\s]*)$/i,
  179. /^(.+[^-\s])(\s+-\s+)(\w[\w\s]+\sRemaster[\w\s]*)$/i,
  180. /^(.+[^-\s])(\s+-\s+)(\w[\w\s]+\sDisc[\w\s]*)$/i,
  181. /^(.+[^-\s])(\s+-\s+)(\w[\w\s]+\sCD[\w\s]*)$/i,
  182. /^(.+[^-\s])(\s+-\s+)(Deluxe[\w\s]*)$/i,
  183. /^(.+[^-\s])(\s+-\s+)(Super[\w\s]*)$/i,
  184. /^(.+[^-\s])(\s+-\s+)(Remaster[\w\s]*)$/i,
  185. /^(.+[^-\s])(\s+-\s+)(Music from[\w\s]*)$/i,
  186. /^(.+[^-\s])(\s+-\s+)(EP[\w\s]*)$/i,
  187. /^(.+[^-\s])(\s+-\s+)(Live[\w\s]*)$/i,
  188. /^(.+[^-\s])(\s+-\s+)(single[\w\s]*)$/i,
  189. /^(.+[^-\s])(\s+-\s+)(Explicit[\w\s]*)$/i,
  190. /^(.+[^-\s])(\s+-\s+)(Disc\s[\w\s]+)$/i,
  191. /^(.+[^-\s])(\s+-\s+)(CD\s[\w\s]+)$/i,
  192. /^(.+[^-\s])(\s+)([([][\w\s]+\sEdition[\w\s]*[)\]])$/i,
  193. /^(.+[^-\s])(\s+)([([][\w\s]+\sVersion[\w\s]*[)\]])$/i,
  194. /^(.+[^-\s])(\s+)([([][\w\s]+\sDeluxe[\w\s]*[)\]])$/i,
  195. /^(.+[^-\s])(\s+)([([][\w\s]+\sSuper[\w\s]*[)\]])$/i,
  196. /^(.+[^-\s])(\s+)([([][\w\s]+\sRemaster[\w\s]*[)\]])$/i,
  197. /^(.+[^-\s])(\s+)([([][\w\s]+\sDisc[\w\s]*[)\]])$/i,
  198. /^(.+[^-\s])(\s+)([([][\w\s]+\sCD[\w\s]*[)\]])$/i,
  199. /^(.+[^-\s])(\s+)([([]Deluxe[\w\s]*[)\]])$/i,
  200. /^(.+[^-\s])(\s+)([([]Super[\w\s]*[)\]])$/i,
  201. /^(.+[^-\s])(\s+)([([]Remaster[\w\s]*[)\]])$/i,
  202. /^(.+[^-\s])(\s+)([([]Music from[\w\s]*[)\]])$/i,
  203. /^(.+[^-\s])(\s+)([([]EP[)\]])$/i,
  204. /^(.+[^-\s])(\s+)([([]Live[)\]])$/i,
  205. /^(.+[^-\s])(\s+)([([]single[)\]])$/i,
  206. /^(.+[^-\s])(\s+)([([]Explicit[)\]])$/i,
  207. /^(.+[^-\s])(\s+)([([]Disc\s[\w\s]+[)\]])$/i,
  208. /^(.+[^-\s])(\s+)([([]CD\s[\w\s]+[)\]])$/i,
  209. /^(.+[^-\s])(\s+)(EP[\d\s]*)$/i
  210. ]; // ( ... bonus CD), (single),... ?
  211. for (let element of regs) {
  212. let m = title.match(element);
  213. // 0: full (= basic+spacer+extension)
  214. // 1: basic
  215. // 2: spacer
  216. // 3: extension
  217. if (m!==null && m.length===4) {
  218. rtval.basic = m[1];
  219. rtval.spacer = m[2];
  220. rtval.extension = m[3];
  221. break; // return rtval;
  222. }
  223. }
  224. return rtval;
  225. }
  226. function albumCompoundLinkTag(artistname, artistlink, title, albumlink) {
  227. title = splitAlbumTitle(title);
  228. if (title.extension) {
  229. let shortAlbumlink = artistlink + '/' + encodeURIComponent(title.basic).replace(/%20/g, '+') + '/';
  230. return '<a href="' + shortAlbumlink + '" class="link-block-target" title="' + artistname + ' — ' + title.basic + '">' + title.basic + '</a><span class="albumextension">' + title.spacer + '<a href="' + albumlink + '" class="link-block-target" title="' + artistname + ' — ' + title.full + '">' + title.extension + '</a></span>';
  231. } else {
  232. return '<a href="' + albumlink + '" class="link-block-target" title="' + artistname + ' — ' + title.full + '">' + title.full + '</a>';
  233. }
  234. }
  235.  
  236. let tlists = document.querySelectorAll('section#recent-tracks-section table.chartlist tbody, section.tracklist-section tbody');
  237. linkr.log('tlists.length='+tlists.length);
  238. for (let j=0; j<tlists.length; j++) {
  239. linkr.log('Loop with tlists['+j+'].');
  240. let rows = tlists[j].querySelectorAll('tr.js-focus-controls-container');
  241. if (rows && rows.length > 2) {
  242. linkr.log('tlists['+j+'] has ' + rows.length + ' rows');
  243. let loopstart=1;
  244. if (j===0 && rows[0].classList.contains('now-scrobbling')) {
  245. // loopstart=2; // Uncomment this to prevent album-header at very top of Recent Tracks if the 1st row is a currently a scrobbling (yellow) track
  246. }
  247. for (let i = loopstart; i < rows.length; i++) {
  248. linkr.log('for-loop. i=' + i);
  249. if (i===1 || !rows[i - 2].classList.contains('albumlink-row')) {
  250. linkr.log('for-loop. i=' + i + ' og i-2 er IKKE allerede albumlink-row');
  251. if (i===2) { // for i=2, extra logging...
  252. linkr.log('for-loop. altvalue(rows[i])=' + altvalue(rows[i]));
  253. linkr.log('for-loop. altvalue(rows[i-1])=' + altvalue(rows[i-1]));
  254. linkr.log('for-loop. altvalue(rows[i-2])=' + altvalue(rows[i-2]));
  255. }
  256. if ( altvalue(rows[i]) &&
  257. altvalue(rows[i - 1]) &&
  258. altvalue(rows[i]).toLowerCase() === altvalue(rows[i - 1]).toLowerCase() &&
  259. (i===1 || altvalue(rows[i - 2]) === null || altvalue(rows[i]).toLowerCase() !== altvalue(rows[i - 2]).toLowerCase()) ) {
  260. linkr.log('for-loop. i=' + i + ' og vi har fundet en album-gruppes start');
  261. // TRY to get albumartist right even when misc. featured artists on album tracks:
  262. let bestindex = i-1;
  263. let artistlinkelem = rows[bestindex].querySelector('td.chartlist-artist > a');
  264. let albumcoverelem = rows[bestindex].querySelector('td.chartlist-image > a > img');
  265. let artistname = artistlinkelem.innerText;
  266. let tracks = [{absindex: bestindex, artistname: artistname, coverurl: (albumcoverelem ? albumcoverelem.src : null)}];
  267. for (let k=i; k < rows.length; k++) {
  268. if (altvalue(rows[i-1]).toLowerCase() !== altvalue(rows[k]).toLowerCase()) break; // new album
  269. artistlinkelem = rows[k].querySelector('td.chartlist-artist > a');
  270. albumcoverelem = rows[k].querySelector('td.chartlist-image > a > img');
  271. tracks.push({absindex: k, artistname: artistlinkelem.innerText, coverurl: (albumcoverelem ? albumcoverelem.src : null)});
  272. if (rows[k].querySelector('td.chartlist-artist > a').innerText.length < artistname.length) {
  273. bestindex = k;
  274. artistname = artistlinkelem.innerText;
  275. }
  276. // linkr.log('*** k='+k+': altvalue='+altvalue(rows[k])+', artist='+ rows[k].querySelector('td.chartlist-name span.chartlist-artists > a').textContent, true)
  277. }
  278. let artistlink = rows[bestindex].querySelector('td.chartlist-artist > a');
  279. let albumtitle = altvalue(rows[bestindex]);
  280. let albumcover = rows[bestindex].querySelector('td.chartlist-image > a > img');
  281. if (albumcover) albumcover=albumcover.src;
  282. if (artistlink) {
  283. if (tracks.reduce(function (x, y) {
  284. return x && containing(y.artistname, artistname);
  285. }, true)) { //y.artistname.includes(artistname)
  286. artistlink = artistlink.href;
  287. linkr.log('*** [before split()]: All albumtracks on "' + albumtitle + '" has "' + artistname + '" contained in trackartists');
  288. } else {
  289. artistname = artistname.split(',')[0];
  290. artistlink = artistlink.href.split(',')[0];
  291. if (tracks.reduce(function (x, y) {
  292. return x && containing(y.artistname, artistname);
  293. }, true)) {
  294. linkr.log('*** [after split()]: All albumtracks on "' + albumtitle + '" has "' + artistname + '" contained in trackartists');
  295. } else {
  296. // Looks like we have a "Various Artists"...
  297. artistname = 'Various Artists';
  298. artistlink = '/music/Various+Artists';
  299. linkr.log('*** [far after split()]: Seems "' + albumtitle + '" is a "Various Artists" album...');
  300. }
  301. }
  302. let albumlink = artistlink + '/' + encodeURIComponent(albumtitle).replace(/%20/g, '+') + '/';
  303. let tr = document.createElement('tr');
  304. tr.classList.add('albumlink-row', 'js-focus-controls-container', 'chartlist-row'); // https://c1.staticflickr.com/3/2821/32308516104_dc32a69ba0_o.png // or http://www.rockland.dk/img/album244c.png // or https://images1-fcus-opensocial.googleusercontent.com/gadgets/proxy?url=http%3A%2F%2Fwww.rockland.dk%2Fimg%2Falbum244c.png&container=focus&resize_w=244&refresh=3600
  305. tr.setAttribute('data-ajax-form-state','');
  306. tr.setAttribute('data-recenttrack-id','');
  307. tr.setAttribute('data-timestamp','');
  308. tr.innerHTML = '<td class="chartlist-play"></td><td class="chartlist-image"><a class="cover-art" href="' + albumlink + '"><img alt="" title="' + albumtitle + '" src="' + albumcover + '" class="cover-art"></a></td><td class="chartlist-loved"><a class="albumicon" href="' + albumlink.replace(/\/user\/[^/]+\/library\//, '/') + '"></a></td><td class="chartlist-name" colspan="2"><span class="chartlist-ellipsis-wrap"><span class="chartlist-artists"><a href="' + artistlink + '" title="' + artistname + '">' + artistname + '</a></span><span class="artist-name-spacer"> — </span>' + albumCompoundLinkTag(artistname, artistlink, albumtitle, albumlink) + '</span></td><td class="chartlist-buylinks focus-control"><div class="lazy-buylinks"><button class="disclose-trigger lazy-buylinks-toggle" aria-expanded="false" aria-controls="buylinks-linkr-' + i + '" data-lazy-buylink="" data-disclose-lazy-buylinks="" data-lazy-buylink-url="' + albumlink.replace(/\/user\/[^/]+\/library\//, '/') + '/+partial/buylinks" data-dropdown-layout-at="center bottom+3">Buy</button><ul id="buylinks-linkr-' + i + '" class="disclose-hide dropdown-menu-clickable buylinks-dropdown-menu"><li class="menu-loading" data-lazy-buylinks-loading="" aria-live="polite">Loading</li></ul></div></td><td class="chartlist-more">&nbsp;</td><td class="chartlist-timestamp">&nbsp;</td>';
  309. linkr.log('Now trying to add tr...');
  310. tlists[j].insertBefore(tr, rows[i - 1]);
  311. linkr.log('and should be added now!?');
  312. i += 1; // or http://stackoverflow.com/questions/8766910/is-there-a-loop-start-over ?
  313. }
  314. }
  315. }
  316. }
  317. } else {
  318. linkr.log('but not enough children found...');
  319. }
  320. }
  321. linkr.log('linking function about to run linkify and sidebar...');
  322. // extras here...
  323. linkr.linkifySidebar();
  324. setTimeout(linkr.tapmusicSidebar, 100);
  325. linkr.log('Natural exit from linking function...', linkr.INFO);
  326. linkr.linking_running = false;
  327. },
  328. setupObserver: function () {
  329. // linkr.log('Running setupObserver()');
  330. linkr.insertStyle();
  331. linkr.observed = document.querySelector('table.chartlist > tbody');
  332. if (!linkr.observed?.classList) {
  333. linkr.log('Object to observe NOT found - re-trying later...');
  334. } else if (linkr.observed.classList.contains('hasObserver')) {
  335. // linkr.log('Everything is okay! - But checking again later...');
  336. } else {
  337. linkr.linking();
  338. linkr.log('Now creating Observer...', linkr.INFO);
  339. let observer = new MutationObserver(linkr.linking);
  340. let config = {attributes: false, childList: true, subtree: false, characterData: false};
  341. linkr.log('Now starting Observer...', linkr.INFO);
  342. observer.observe(linkr.observed, config);
  343. linkr.log('Observer added and running...');
  344. linkr.observed.classList.add('hasObserver');
  345. linkr.log('hasObserver class added...', linkr.INFO);
  346. }
  347. },
  348. linkifyStr: function (str, attributes) {
  349. let a1 = '<a ' + (attributes ? attributes+' ' : '') + 'href="';
  350. let a2 = '">';
  351. let a3 = '</a>';
  352. let url = /(^|\s|\(|>)([fhtpsr]+:\/\/[^\s]+?)([.,;\]"]?(\s|$|\))|<)/igm;
  353. // var url2 = /(^|\s|\()([fhtpsr]+:\/\/[^\s]+?)([\.,;\]"]?(\s|$|\)))/igm;
  354. // This looks a bit weird, but we have to do a replace twice to catch URLs
  355. // which immediately follow each other. This is because leading and trailing
  356. // whitespaces are part of the expressions, and if a trailing whitespace of
  357. // a match needs to be a leading whitespace of the next URL to match, it
  358. // won't be caught.
  359. let s = str.replace(url, '$1' + a1 + '$2' + a2 + '$2' + a3 + '$3$4');
  360. return(s.replace(url, '$1' + a1 + '$2' + a2 + '$2' + a3 + '$3$4'));
  361. },
  362. linkifySidebar: function() {
  363. if (linkr.linkifyEnabled) {
  364. let a = document.querySelectorAll('.about-me-sidebar p');
  365. for (const element of a) {
  366. element.innerHTML = linkr.linkifyStr(element.innerHTML);
  367. }
  368. }
  369. },
  370. tapmusicSidebar: function() {
  371. if (linkr.collagetype) {
  372. // /user/userid
  373. let pattern = /^\/user\/([^/]+)/i;
  374. let result = RegExp(pattern).exec(window.location.pathname);
  375. if (result) {
  376. linkr.log('tapmusicSidebar(): url match med userid=' + result[1]);
  377. let b = document.querySelector('.stationlinks');
  378. if (b && !document.getElementById('tapmusic')) {
  379. // Via google cache:
  380. // b.insertAdjacentHTML('beforeend', '<div style="margin:1em 0;width:300px" id="tapmusic"><div class="tapcollage"><img src="https://images1-focus-opensocial.googleusercontent.com/gadgets/proxy?url=https%3A%2F%2Fwww.tapmusic.net%2Fcollage.php%3Fuser%3D' + result[1] + '%26type%3D'+linkr.collagetype+'%26size%3D2x6&container=focus&resize_w=300&refresh=3600" alt="If this text is visible, tapmusic.net might be slow or not responding - or the profile you are looking at does not have recent scrobbles to generate a collage from... But sometimes a simple re-load of page also helps." style="display:block;margin:0;padding:0;width:300px;height:900px" decoding="async" /></div><div class="tapcredit"><em title="Album collage by www.tapmusic.net/lastfm - Embedded by Stig\'s Last.fm Album Linkr">Album collage by <a href="http://www.tapmusic.net/lastfm/">www.tapmusic.net/lastfm/</a></em></div></div>');
  381. // Directly from tapmusic:
  382. b.insertAdjacentHTML('beforeend', '<div style="margin:1em 0;width:300px" id="tapmusic"><div class="tapcollage"><img src="https://tapmusic.net/collage.php?user=' + result[1] + '&type='+linkr.collagetype+'&size=2x6" alt="If this text is visible, tapmusic.net might be slow or not responding - or the profile you are looking at does not have recent scrobbles to generate a collage from... But sometimes a simple re-load of page also helps." style="display:block;margin:0;padding:0;width:300px;height:900px" decoding="async" /></div><div class="tapcredit"><em title="Album collage by tapmusic.net/lastfm - Embedded by Stig\'s Last.fm Album Linkr">Album collage by <a href="https://tapmusic.net/lastfm/?utm_source=https%3A%2F%2Fgf.qytechs.cn%2Fscripts%2F21153">tapmusic.net/lastfm/</a></em></div></div>');
  383. }
  384. } else {
  385. linkr.log('tapmusicSidebar(): returnerer false! reg-pattern fandt ikke match i pathname=' + window.location.pathname);
  386. }
  387. }
  388. },
  389. boarding: function() {
  390. if (GMC.info?.script?.version) {
  391. const box = document.createElement('dialog');
  392. if (box?.showModal && GMC.info.script.version !== GMC.getValue('boardingversion')) {
  393. box.innerHTML =
  394. `
  395. <style>dialog {background-color: #fff9e5; max-width: 60dvw; font-family: sans-serif} dialog button {background-color: #fff9e5; padding: 2px 4px; margin-top: .7em; border: 2px solid #000}</style>
  396. <p>You are now using userscript (GMScript)
  397. <strong><em><a href="https://gf.qytechs.cn/scripts/21153-stig-s-last-fm-album-linkr" target="_blank">Stig's Last.fm Album Linkr</a></em>
  398. version <em>${GMC.info.script.version}</em></strong>. It is the first update of this script in
  399. nearly 4 years! This version primarily improves detection and handling of "extended" album-titles.</p>
  400. <p>Do you like the red album-headers that <em>Album Linkr</em> adds to playlists on the Last.fm site?
  401. If you have a homepage/blog in need of a "scrobbles widget", you might want to take a look at
  402. <strong><em><a href="https://lastfm-widgets.deno.dev/" target="_blank">Tracks</a></em></strong>
  403. - a new web-widget I have created that should work on most websites. And it also (optionally)
  404. features red album-headers🙂</p>
  405. <p>This message should normally only be shown once.</p>
  406. <button autofocus>Close</button>
  407. `;
  408. document.body.appendChild(box);
  409. GMC.setValue('boardingversion', GMC.info.script.version);
  410. box.showModal();
  411. box.addEventListener('click', (ev) => {
  412. ev.stopPropagation();
  413. ev.preventDefault();
  414. box.close();
  415. });
  416. }
  417. }
  418. },
  419. init: function () {
  420. linkr.log('Running init() on last.fm with readyState = ' + document.readyState, linkr.INFO);
  421. if (!(linkr.observed?.classList?.contains('hasObserver'))) {
  422. linkr.loadSettings();
  423. linkr.setupObserver();
  424. setInterval(linkr.setupObserver, 2000);
  425. GMC.registerMenuCommand("Album Collages - Disabled", linkr.collageOff, {
  426. accessKey: "D",
  427. type: "radio",
  428. name: 'collage',
  429. checked: (linkr.collagetype === '')
  430. });
  431. GMC.registerMenuCommand("Album Collages - 7 Days", linkr.collage7day, {
  432. accessKey: "7",
  433. type: "radio",
  434. name: 'collage',
  435. checked: (linkr.collagetype === '7day')
  436. });
  437. GMC.registerMenuCommand("Album Collages - 1 Month", linkr.collage1month, {
  438. accessKey: "1",
  439. type: "radio",
  440. name: 'collage',
  441. checked: (linkr.collagetype === '1month')
  442. });
  443. GMC.registerMenuCommand("Album Collages - 3 Months", linkr.collage3month, {
  444. accessKey: "3",
  445. type: "radio",
  446. name: 'collage',
  447. checked: (linkr.collagetype === '3month')
  448. });
  449. GMC.registerMenuCommand("Album Collages - 6 Months", linkr.collage6month, {
  450. accessKey: "6",
  451. type: "radio",
  452. name: 'collage',
  453. checked: (linkr.collagetype === '6month')
  454. });
  455. GMC.registerMenuCommand("Album Collages - 1 Year", linkr.collage12month, {
  456. accessKey: "Y",
  457. type: "radio",
  458. name: 'collage',
  459. checked: (linkr.collagetype === '12month')
  460. });
  461. GMC.registerMenuCommand("Album Collages - Overall", linkr.collageOverall, {
  462. accessKey: "O",
  463. type: "radio",
  464. name: 'collage',
  465. checked: (linkr.collagetype === 'overall')
  466. });
  467. GMC.registerMenuCommand("Linkify About Me section", linkr.toggleLinkifyEnabled, {
  468. accessKey: "L",
  469. type: "checkbox",
  470. checked: (linkr.linkifyEnabled)
  471. });
  472. GMC.registerMenuCommand("Collapse the top", linkr.toggleCollapseTop, {
  473. accessKey: "C",
  474. type: "checkbox",
  475. checked: (linkr.collapseTop)
  476. });
  477.  
  478. linkr.boarding();
  479. }
  480. }
  481. };
  482.  
  483. linkr.log('Userscript running at readyState: ' + document.readyState, linkr.INFO);
  484. window.addEventListener('DOMContentLoaded', linkr.init, false);
  485. // window.addEventListener('load', linkr.init, false);
  486. window.addEventListener('pageshow', linkr.init, false);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址