scRYMble

Visit a release page on rateyourmusic.com and scrobble the songs you see!

  1. // ==UserScript==
  2. // @name scRYMble
  3. // @license MIT
  4. // @version 2.20250109031512
  5. // @description Visit a release page on rateyourmusic.com and scrobble the songs you see!
  6. // @author fidwell
  7. // @icon https://e.snmc.io/2.5/img/sonemic.png
  8. // @namespace https://github.com/fidwell/scRYMble
  9. // @include https://rateyourmusic.com/release/*
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_xmlhttpRequest
  13. // @require https://update.greasyfork.org/scripts/130/10066/Portable%20MD5%20Function.js
  14. // ==/UserScript==
  15. 'use strict';
  16.  
  17. class HttpResponse {
  18. constructor(raw) {
  19. this.status = raw.status;
  20. this.statusText = raw.statusText;
  21. this.responseText = raw.responseText;
  22. this.lines = raw.responseText.split("\n");
  23. }
  24. get isOkStatus() {
  25. return this.lines[0] === "OK";
  26. }
  27. get sessionId() {
  28. return this.lines[1];
  29. }
  30. get nowPlayingUrl() {
  31. return this.lines[2];
  32. }
  33. get submitUrl() {
  34. return this.lines[3];
  35. }
  36. }
  37.  
  38. function httpGet(url, onload) {
  39. GM_xmlhttpRequest({
  40. method: "GET",
  41. url,
  42. headers: {
  43. "User-agent": "Mozilla/4.0 (compatible) Greasemonkey"
  44. },
  45. onload: (responseRaw) => onload(new HttpResponse(responseRaw))
  46. });
  47. }
  48. function httpPost(url, data, onload) {
  49. GM_xmlhttpRequest({
  50. method: "POST",
  51. url,
  52. data,
  53. headers: {
  54. "User-agent": "Mozilla/4.0 (compatible) Greasemonkey",
  55. "Content-type": "application/x-www-form-urlencoded"
  56. },
  57. onload: (responseRaw) => onload(new HttpResponse(responseRaw))
  58. });
  59. }
  60.  
  61. function fetch_unix_timestamp() {
  62. return parseInt(new Date().getTime().toString().substring(0, 10));
  63. }
  64. function stripAndClean(input) {
  65. input = input
  66. .replace("&", "")
  67. .replace("\n", " ");
  68. while (input.indexOf(" ") >= 0) {
  69. input = input.replace(" ", " ");
  70. }
  71. while (input.startsWith(" - ")) {
  72. input = input.substring(3);
  73. }
  74. while (input.startsWith("- ")) {
  75. input = input.substring(2);
  76. }
  77. return input.trim();
  78. }
  79.  
  80. function handshake(ui, callback) {
  81. const username = ui.username;
  82. const password = ui.password;
  83. GM_setValue("user", username);
  84. GM_setValue("pass", password);
  85. const timestamp = fetch_unix_timestamp();
  86. const auth = hex_md5(`${hex_md5(password)}${timestamp}`);
  87. const handshakeURL = `http://post.audioscrobbler.com/?hs=true&p=1.2&c=scr&v=1.0&u=${username}&t=${timestamp}&a=${auth}`;
  88. httpGet(handshakeURL, callback);
  89. }
  90.  
  91. class rymUi {
  92. constructor() {
  93. this.albumTitleClass = ".album_title";
  94. this.byArtistProperty = "byArtist";
  95. this.creditedNameClass = "credited_name";
  96. this.trackElementId = "tracks";
  97. this.tracklistDurationClass = ".tracklist_duration";
  98. this.tracklistLineClass = "tracklist_line";
  99. this.tracklistNumClass = ".tracklist_num";
  100. this.tracklistTitleClass = ".tracklist_title";
  101. this.tracklistArtistClass = ".artist";
  102. this.tracklistRenderedTextClass = ".rendered_text";
  103. //#endregion
  104. }
  105. get isVariousArtists() {
  106. const artist = this.pageArtist;
  107. return artist.indexOf("Various Artists") > -1 ||
  108. artist.indexOf(" / ") > -1;
  109. }
  110. get pageArtist() {
  111. var _a;
  112. return (_a = this.multipleByArtists) !== null && _a !== void 0 ? _a : this.singleByArtist;
  113. }
  114. get pageAlbum() {
  115. var _a, _b;
  116. // Not using innerText because it doesn't work with Jest tests.
  117. const element = document.querySelector(this.albumTitleClass);
  118. return ((_b = (_a = element.firstChild) === null || _a === void 0 ? void 0 : _a.textContent) !== null && _b !== void 0 ? _b : "").trim();
  119. }
  120. get multipleByArtists() {
  121. return Array.from(document.getElementsByClassName(this.creditedNameClass))
  122. .map(x => x)
  123. .map(x => { var _a; return (_a = x.innerText) !== null && _a !== void 0 ? _a : ""; })[1];
  124. }
  125. get singleByArtist() {
  126. return Array.from(document.querySelectorAll(`span[itemprop='${this.byArtistProperty}'] > a`))
  127. .map(e => this.parseArtistLink(e))
  128. .join(" / ");
  129. }
  130. parseArtistLink(element) {
  131. return Array.from(element.childNodes)
  132. .filter(node => node.nodeType === 3) // Node.TEXT_NODE
  133. .map(node => { var _a, _b; return (_b = (_a = node.textContent) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : ""; })
  134. .join("");
  135. }
  136. hasTrackNumber(tracklistLine) {
  137. var _a, _b;
  138. return ((_b = (_a = tracklistLine.querySelector(this.tracklistNumClass)) === null || _a === void 0 ? void 0 : _a.innerHTML) !== null && _b !== void 0 ? _b : "").trim().length > 0;
  139. }
  140. //#region Element getters
  141. get trackListDiv() {
  142. return document.getElementById(this.trackElementId);
  143. }
  144. get tracklistLines() {
  145. var _a;
  146. return Array.from((_a = this.trackListDiv.getElementsByClassName(this.tracklistLineClass)) !== null && _a !== void 0 ? _a : [])
  147. .map(l => l);
  148. }
  149. tracklistLine(checkbox) {
  150. var _a;
  151. return (_a = checkbox.parentElement) === null || _a === void 0 ? void 0 : _a.parentElement;
  152. }
  153. trackName(tracklistLine) {
  154. var _a, _b;
  155. let songTitle = "";
  156. const songTags = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelectorAll("[itemprop=name]");
  157. if (songTags.length > 0) {
  158. const lastSongTag = songTags[songTags.length - 1];
  159. songTitle = ((_a = lastSongTag === null || lastSongTag === void 0 ? void 0 : lastSongTag.textContent) !== null && _a !== void 0 ? _a : "").replace(/\n/g, " ");
  160. // Check if the tag is hiding any artist links; if so, strip them out
  161. const artistLinks = lastSongTag.querySelectorAll(this.tracklistArtistClass);
  162. if (artistLinks.length > 0) {
  163. const renderedTextSpan = lastSongTag.querySelector(this.tracklistRenderedTextClass);
  164. songTitle = renderedTextSpan.innerHTML.replace(/<a[^>]*>.*?<\/a>/g, " ").trim();
  165. }
  166. }
  167. else {
  168. const renderedTextSpan = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelector(this.tracklistRenderedTextClass);
  169. songTitle = (_b = renderedTextSpan === null || renderedTextSpan === void 0 ? void 0 : renderedTextSpan.textContent) !== null && _b !== void 0 ? _b : "";
  170. }
  171. return stripAndClean(songTitle);
  172. }
  173. trackArtist(tracklistLine) {
  174. var _a, _b;
  175. const artistTags = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelectorAll(this.tracklistArtistClass);
  176. if (artistTags.length === 0)
  177. return "";
  178. if (artistTags.length === 1) {
  179. return (_a = artistTags[0].textContent) !== null && _a !== void 0 ? _a : "";
  180. }
  181. // Multiple artists
  182. const entireSpan = tracklistLine.querySelector(this.tracklistTitleClass);
  183. const entireText = ((_b = entireSpan.textContent) !== null && _b !== void 0 ? _b : "").replace(/\n/g, " ");
  184. const dashIndex = entireText.indexOf(" - ");
  185. return entireText.substring(0, dashIndex);
  186. }
  187. trackDuration(tracklistLine) {
  188. var _a;
  189. const durationElement = tracklistLine === null || tracklistLine === void 0 ? void 0 : tracklistLine.querySelector(this.tracklistDurationClass);
  190. return ((_a = durationElement.textContent) !== null && _a !== void 0 ? _a : "").trim();
  191. }
  192. }
  193.  
  194. class scRYMbleUi {
  195. constructor(rymUi) {
  196. var _a, _b;
  197. this.enabled = false;
  198. this.marqueeId = "scrymblemarquee";
  199. this.progBarId = "progbar";
  200. this.scrobbleNowId = "scrobblenow";
  201. this.scrobbleThenId = "scrobblethen";
  202. this.testId = "scrobbletest";
  203. this.checkboxClass = "scrymblechk";
  204. this.selectAllOrNoneId = "allornone";
  205. this.usernameId = "scrobbleusername";
  206. this.passwordId = "scrobblepassword";
  207. this._rymUi = rymUi;
  208. if (((_b = (_a = this._rymUi.trackListDiv) === null || _a === void 0 ? void 0 : _a.children.length) !== null && _b !== void 0 ? _b : 0) === 0) {
  209. console.log("scRYMble: No track list found.");
  210. }
  211. else {
  212. this.enabled = true;
  213. this.createCheckboxes();
  214. this.createControls();
  215. }
  216. }
  217. get isEnabled() {
  218. return this.enabled;
  219. }
  220. get username() {
  221. return this.usernameInput.value;
  222. }
  223. get password() {
  224. return this.passwordInput.value;
  225. }
  226. createCheckboxes() {
  227. const checkboxTemplate = `<input type="checkbox" class="${this.checkboxClass}" checked="checked">`;
  228. for (const tracklistLine of this._rymUi.tracklistLines) {
  229. if (this._rymUi.hasTrackNumber(tracklistLine)) {
  230. const thisCheckboxElement = document.createElement("span");
  231. thisCheckboxElement.style.float = "left";
  232. thisCheckboxElement.innerHTML = checkboxTemplate;
  233. tracklistLine.prepend(thisCheckboxElement);
  234. }
  235. }
  236. }
  237. createControls() {
  238. var _a;
  239. const eleButtonDiv = document.createElement("div");
  240. eleButtonDiv.innerHTML = `
  241. <table style="border: 0;" cellpadding="0" cellspacing="2px">
  242. <tr>
  243. <td style="width: 112px;">
  244. <input type="checkbox" name="${this.selectAllOrNoneId}" id="${this.selectAllOrNoneId}" style="vertical-align: middle;" checked="checked">&nbsp;
  245. <label for="${this.selectAllOrNoneId}" style="font-size: 60%;">select&nbsp;all/none</label>
  246. <br/>
  247. <table border="2" cellpadding="0" cellspacing="0">
  248. <tr>
  249. <td style="height: 50px; width: 103px; background: url(https://cdn.last.fm/flatness/logo_black.3.png) no-repeat; color: #fff;">
  250. <div class="marquee" style="position: relative; top: 17px; overflow: hidden; white-space: nowrap;">
  251. <span style="font-size: 80%; width: 88px; display: inline-block; animation: marquee 5s linear infinite;" id="${this.marqueeId}">&nbsp;</span>
  252. </div>
  253. </td>
  254. </tr>
  255. <tr>
  256. <td style="background-color: #003;">
  257. <div style="position: relative; background-color: #f00; width: 0; max-height: 5px; left: 0; top: 0;" id="${this.progBarId}">&nbsp;</div>
  258. </td>
  259. </tr>
  260. </table>
  261. </td>
  262. <td>user: <input type="text" size="16" id="${this.usernameId}" value="${GM_getValue("user", "")}" /><br />
  263. pass: <input type="password" size="16" id="${this.passwordId}" value="${GM_getValue("pass", "")}"></input><br />
  264. <input type="button" id="${this.scrobbleNowId}" value="Scrobble in real-time" />
  265. <input type="button" id="${this.scrobbleThenId}" value="Scrobble a previous play" />
  266. <input type="button" id="${this.testId}" value="Test tracklist parsing" style="display: none;" />
  267. </td>
  268. </tr>
  269. </table>`;
  270. eleButtonDiv.style.textAlign = "right";
  271. (_a = this._rymUi.trackListDiv) === null || _a === void 0 ? void 0 : _a.after(eleButtonDiv);
  272. this.allOrNoneCheckbox.addEventListener("click", () => this.allOrNoneClick(), true);
  273. const marqueeStyle = document.createElement("style");
  274. document.head.appendChild(marqueeStyle);
  275. marqueeStyle.textContent = `
  276. @keyframes marquee {
  277. 0% { transform: translateX(100%); }
  278. 100% { transform: translateX(-100%); }
  279. }`;
  280. }
  281. hookUpScrobbleNow(startScrobble) {
  282. this.scrobbleNowButton.addEventListener("click", startScrobble, true);
  283. }
  284. hookUpScrobbleThen(handshakeBatch) {
  285. this.scrobbleThenButton.addEventListener("click", handshakeBatch, true);
  286. }
  287. hookUpScrobbleTest(callback) {
  288. this.scrobbleTestButton.addEventListener("click", callback, true);
  289. }
  290. setMarquee(value) {
  291. this.marquee.innerHTML = value;
  292. }
  293. setProgressBar(percentage) {
  294. if (percentage >= 0 && percentage <= 100) {
  295. this.progressBar.style.width = `${percentage}%`;
  296. }
  297. }
  298. allOrNoneClick() {
  299. window.setTimeout(() => this.allOrNoneAction(), 10);
  300. }
  301. allOrNoneAction() {
  302. for (const checkbox of this.checkboxes) {
  303. checkbox.checked = this.allOrNoneCheckbox.checked;
  304. }
  305. }
  306. elementsOnAndOff(state) {
  307. if (state) {
  308. this.scrobbleNowButton.removeAttribute("disabled");
  309. this.usernameInput.removeAttribute("disabled");
  310. this.passwordInput.removeAttribute("disabled");
  311. }
  312. else {
  313. this.scrobbleNowButton.setAttribute("disabled", "disabled");
  314. this.usernameInput.setAttribute("disabled", "disabled");
  315. this.passwordInput.setAttribute("disabled", "disabled");
  316. }
  317. for (const checkbox of this.checkboxes) {
  318. if (state) {
  319. checkbox.removeAttribute("disabled");
  320. }
  321. else {
  322. checkbox.setAttribute("disabled", "disabled");
  323. }
  324. }
  325. }
  326. elementsOff() {
  327. this.elementsOnAndOff(false);
  328. }
  329. elementsOn() {
  330. this.elementsOnAndOff(true);
  331. }
  332. //#region Element getters
  333. get allOrNoneCheckbox() {
  334. return document.getElementById(this.selectAllOrNoneId);
  335. }
  336. get scrobbleNowButton() {
  337. return document.getElementById(this.scrobbleNowId);
  338. }
  339. get scrobbleThenButton() {
  340. return document.getElementById(this.scrobbleThenId);
  341. }
  342. get scrobbleTestButton() {
  343. return document.getElementById(this.testId);
  344. }
  345. get marquee() {
  346. return document.getElementById(this.marqueeId);
  347. }
  348. get progressBar() {
  349. return document.getElementById(this.progBarId);
  350. }
  351. get usernameInput() {
  352. return document.getElementById(this.usernameId);
  353. }
  354. get passwordInput() {
  355. return document.getElementById(this.passwordId);
  356. }
  357. get checkboxes() {
  358. return document.getElementsByClassName(this.checkboxClass);
  359. }
  360. }
  361.  
  362. class ScrobbleRecord {
  363. constructor(trackName, artist, duration) {
  364. this.artist = artist;
  365. this.trackName = trackName;
  366. const durastr = duration.trim();
  367. const colon = durastr.indexOf(":");
  368. if (colon !== -1) {
  369. const minutes = parseInt(durastr.substring(0, colon));
  370. const seconds = parseInt(durastr.substring(colon + 1));
  371. this.duration = minutes * 60 + seconds;
  372. }
  373. else {
  374. this.duration = 180;
  375. }
  376. this.time = 0;
  377. }
  378. }
  379.  
  380. function buildListOfSongsToScrobble(_rymUi, _scRYMbleUi) {
  381. const toScrobble = [];
  382. Array.from(_scRYMbleUi.checkboxes).forEach(checkbox => {
  383. if (checkbox.checked) {
  384. toScrobble[toScrobble.length] = parseTracklistLine(_rymUi, checkbox);
  385. }
  386. });
  387. return toScrobble;
  388. }
  389. function parseTracklistLine(rymUi, checkbox) {
  390. const tracklistLine = rymUi.tracklistLine(checkbox);
  391. const pageArtist = rymUi.pageArtist;
  392. let songTitle = rymUi.trackName(tracklistLine);
  393. let artist = pageArtist;
  394. const duration = rymUi.trackDuration(tracklistLine);
  395. if (rymUi.isVariousArtists) {
  396. artist = rymUi.trackArtist(tracklistLine);
  397. if (artist.length === 0) {
  398. artist = pageArtist.indexOf("Various Artists") > -1
  399. ? rymUi.pageAlbum
  400. : pageArtist; // Probably a collaboration release, like a classical work.
  401. }
  402. }
  403. else {
  404. const trackArtist = rymUi.trackArtist(tracklistLine);
  405. if (trackArtist.length > 0) {
  406. artist = trackArtist;
  407. }
  408. }
  409. if (songTitle.toLowerCase() === "untitled" ||
  410. songTitle.toLowerCase() === "untitled track" ||
  411. songTitle === "") {
  412. songTitle = "[untitled]";
  413. }
  414. return new ScrobbleRecord(songTitle, artist, duration);
  415. }
  416.  
  417. const _rymUi = new rymUi();
  418. const _scRYMbleUi = new scRYMbleUi(_rymUi);
  419. let toScrobble = [];
  420. let currentlyScrobbling = -1;
  421. let sessID = "";
  422. let submitURL = "";
  423. let npURL = "";
  424. let currTrackDuration = 0;
  425. let currTrackPlayTime = 0;
  426. function confirmBrowseAway(oEvent) {
  427. if (currentlyScrobbling !== -1) {
  428. oEvent.preventDefault();
  429. return "You are currently scrobbling a record. Leaving the page now will prevent future tracks from this release from scrobbling.";
  430. }
  431. return "";
  432. }
  433. function acceptSubmitResponse(responseDetails, isBatch) {
  434. if (responseDetails.status === 200) {
  435. if (!responseDetails.isOkStatus) {
  436. alertSubmitFailed(responseDetails);
  437. }
  438. }
  439. else {
  440. alertSubmitFailed(responseDetails);
  441. }
  442. if (isBatch) {
  443. _scRYMbleUi.setMarquee("Scrobbled OK!");
  444. }
  445. else {
  446. scrobbleNextSong();
  447. }
  448. }
  449. function alertSubmitFailed(responseDetails) {
  450. alert(`Track submit failed: ${responseDetails.status} ${responseDetails.statusText}\n\nData:\n${responseDetails.responseText}`);
  451. }
  452. function acceptSubmitResponseSingle(responseDetails) {
  453. acceptSubmitResponse(responseDetails, false);
  454. }
  455. function acceptSubmitResponseBatch(responseDetails) {
  456. acceptSubmitResponse(responseDetails, true);
  457. }
  458. function acceptNPResponse(responseDetails) {
  459. if (responseDetails.status === 200) {
  460. if (!responseDetails.isOkStatus) {
  461. alertSubmitFailed(responseDetails);
  462. }
  463. }
  464. else {
  465. alertSubmitFailed(responseDetails);
  466. }
  467. }
  468. function submitTracksBatch(sessID, submitURL) {
  469. toScrobble = buildListOfSongsToScrobble(_rymUi, _scRYMbleUi);
  470. if (toScrobble === null)
  471. return;
  472. let currTime = fetch_unix_timestamp();
  473. const hoursFudgeStr = prompt("How many hours ago did you listen to this?");
  474. if (hoursFudgeStr !== null) {
  475. const album = _rymUi.pageAlbum;
  476. const hoursFudge = parseFloat(hoursFudgeStr);
  477. if (!isNaN(hoursFudge)) {
  478. currTime = currTime - hoursFudge * 60 * 60;
  479. }
  480. for (let i = toScrobble.length - 1; i >= 0; i--) {
  481. currTime = currTime * 1 - toScrobble[i].duration * 1;
  482. toScrobble[i].time = currTime;
  483. }
  484. let outstr = `Artist: ${_rymUi.pageArtist}\nAlbum: ${album}\n`;
  485. for (const song of toScrobble) {
  486. outstr = `${outstr}${song.trackName} (${song.duration})\n`;
  487. }
  488. const postdata = {};
  489. for (let i = 0; i < toScrobble.length; i++) {
  490. postdata[`a[${i}]`] = toScrobble[i].artist;
  491. postdata[`t[${i}]`] = toScrobble[i].trackName;
  492. postdata[`b[${i}]`] = album;
  493. postdata[`n[${i}]`] = `${i + 1}`;
  494. postdata[`l[${i}]`] = `${toScrobble[i].duration}`;
  495. postdata[`i[${i}]`] = `${toScrobble[i].time}`;
  496. postdata[`o[${i}]`] = "P";
  497. postdata[`r[${i}]`] = "";
  498. postdata[`m[${i}]`] = "";
  499. }
  500. postdata["s"] = sessID;
  501. const postdataStr = Object.entries(postdata)
  502. .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
  503. .join("&");
  504. httpPost(submitURL, postdataStr, acceptSubmitResponseBatch);
  505. }
  506. }
  507. function startScrobble() {
  508. currentlyScrobbling = -1;
  509. currTrackDuration = 0;
  510. currTrackPlayTime = 0;
  511. _scRYMbleUi.elementsOff();
  512. toScrobble = buildListOfSongsToScrobble(_rymUi, _scRYMbleUi);
  513. scrobbleNextSong();
  514. }
  515. function resetScrobbler() {
  516. currentlyScrobbling = -1;
  517. currTrackDuration = 0;
  518. currTrackPlayTime = 0;
  519. _scRYMbleUi.setMarquee("&nbsp;");
  520. _scRYMbleUi.setProgressBar(0);
  521. toScrobble = [];
  522. _scRYMbleUi.elementsOn();
  523. }
  524. function scrobbleNextSong() {
  525. currentlyScrobbling++;
  526. if (currentlyScrobbling === toScrobble.length) {
  527. resetScrobbler();
  528. }
  529. else {
  530. window.setTimeout(timertick, 10);
  531. handshake(_scRYMbleUi, acceptHandshakeSingle);
  532. }
  533. }
  534. function submitThisTrack() {
  535. const postdata = {};
  536. const i = 0;
  537. const currTime = fetch_unix_timestamp();
  538. postdata[`a[${i}]`] = toScrobble[currentlyScrobbling].artist;
  539. postdata[`t[${i}]`] = toScrobble[currentlyScrobbling].trackName;
  540. postdata[`b[${i}]`] = _rymUi.pageAlbum;
  541. postdata[`n[${i}]`] = `${currentlyScrobbling + 1}`;
  542. postdata[`l[${i}]`] = `${toScrobble[currentlyScrobbling].duration}`;
  543. postdata[`i[${i}]`] = `${currTime - toScrobble[currentlyScrobbling].duration}`;
  544. postdata[`o[${i}]`] = "P";
  545. postdata[`r[${i}]`] = "";
  546. postdata[`m[${i}]`] = "";
  547. postdata["s"] = sessID;
  548. const postdataStr = Object.entries(postdata)
  549. .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
  550. .join("&");
  551. httpPost(submitURL, postdataStr, acceptSubmitResponseSingle);
  552. }
  553. function npNextTrack() {
  554. const postdata = {};
  555. postdata["a"] = toScrobble[currentlyScrobbling].artist;
  556. postdata["t"] = toScrobble[currentlyScrobbling].trackName;
  557. postdata["b"] = _rymUi.pageAlbum;
  558. postdata["n"] = `${currentlyScrobbling + 1}`;
  559. postdata["l"] = `${toScrobble[currentlyScrobbling].duration}`;
  560. postdata["m"] = "";
  561. postdata["s"] = sessID;
  562. currTrackDuration = toScrobble[currentlyScrobbling].duration;
  563. currTrackPlayTime = 0;
  564. _scRYMbleUi.setMarquee(toScrobble[currentlyScrobbling].trackName);
  565. const postdataStr = Object.entries(postdata)
  566. .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
  567. .join("&");
  568. httpPost(npURL, postdataStr, acceptNPResponse);
  569. }
  570. function timertick() {
  571. let again = true;
  572. if (currentlyScrobbling !== -1) {
  573. if (currTrackDuration !== 0) {
  574. _scRYMbleUi.setProgressBar(100 * currTrackPlayTime / currTrackDuration);
  575. }
  576. currTrackPlayTime++;
  577. if (currTrackPlayTime === currTrackDuration) {
  578. submitThisTrack();
  579. again = false;
  580. }
  581. }
  582. if (again) {
  583. window.setTimeout(timertick, 1000);
  584. }
  585. }
  586. function acceptHandshakeSingle(responseDetails) {
  587. acceptHandshake(responseDetails, false);
  588. }
  589. function acceptHandshakeBatch(responseDetails) {
  590. acceptHandshake(responseDetails, true);
  591. }
  592. function acceptHandshake(responseDetails, isBatch) {
  593. if (responseDetails.status === 200) {
  594. if (!responseDetails.isOkStatus) {
  595. alertHandshakeFailed(responseDetails);
  596. }
  597. else {
  598. sessID = responseDetails.sessionId;
  599. npURL = responseDetails.nowPlayingUrl;
  600. submitURL = responseDetails.submitUrl;
  601. if (isBatch) {
  602. submitTracksBatch(sessID, submitURL);
  603. }
  604. else {
  605. npNextTrack();
  606. }
  607. }
  608. }
  609. else {
  610. alertHandshakeFailed(responseDetails);
  611. }
  612. }
  613. function alertHandshakeFailed(responseDetails) {
  614. alert(`Handshake failed: ${responseDetails.status} ${responseDetails.statusText}\n\nData:\n${responseDetails.responseText}`);
  615. }
  616. function handshakeBatch() {
  617. handshake(_scRYMbleUi, acceptHandshakeBatch);
  618. }
  619. function scrobbleTest() {
  620. console.log(_rymUi.pageAlbum);
  621. toScrobble = buildListOfSongsToScrobble(_rymUi, _scRYMbleUi);
  622. toScrobble.forEach((song, i) => {
  623. const minutes = Math.floor(song.duration / 60);
  624. const seconds = song.duration % 60;
  625. const secondsStr = `00${seconds}`.slice(-2);
  626. console.log(`${i + 1}. ${song.artist} ${song.trackName} (${minutes}:${secondsStr})`);
  627. });
  628. }
  629. (function () {
  630. if (!_scRYMbleUi.isEnabled) {
  631. return;
  632. }
  633. _scRYMbleUi.hookUpScrobbleNow(startScrobble);
  634. _scRYMbleUi.hookUpScrobbleThen(handshakeBatch);
  635. _scRYMbleUi.hookUpScrobbleTest(scrobbleTest);
  636. window.addEventListener("beforeunload", confirmBrowseAway, true);
  637. })();