Last.fm Bulk Edit

Bulk edit your scrobbles for any artist or album on Last.fm at once.

  1. // ==UserScript==
  2. // @name Last.fm Bulk Edit
  3. // @description Bulk edit your scrobbles for any artist or album on Last.fm at once.
  4. // @version 1.6.2
  5. // @author Rudey
  6. // @homepage https://github.com/RudeySH/lastfm-bulk-edit
  7. // @supportURL https://github.com/RudeySH/lastfm-bulk-edit/issues
  8. // @match https://www.last.fm/*
  9. // @icon https://raw.githubusercontent.com/RudeySH/lastfm-bulk-edit/main/img/icon.png
  10. // @license AGPL-3.0-or-later
  11. // @namespace https://github.com/RudeySH/lastfm-bulk-edit
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/he/1.2.0/he.min.js
  13. // ==/UserScript==
  14.  
  15. /******/ (() => { // webpackBootstrap
  16. /******/ var __webpack_modules__ = ({
  17.  
  18. /***/ 135:
  19. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  20.  
  21. "use strict";
  22.  
  23. Object.defineProperty(exports, "__esModule", ({ value: true }));
  24. exports.delay = delay;
  25. exports.encodeURIComponent2 = encodeURIComponent2;
  26. exports.fetchAndRetry = fetchAndRetry;
  27. const async_mutex_1 = __webpack_require__(693);
  28. function delay(ms) {
  29. return new Promise(resolve => setTimeout(resolve, ms));
  30. }
  31. function encodeURIComponent2(uriComponent) {
  32. return encodeURIComponent(uriComponent).replace(/%20/g, '+');
  33. }
  34. const semaphore = new async_mutex_1.Semaphore(6);
  35. let delayPromise = undefined;
  36. let delayTooManyRequestsMs = 10000;
  37. async function fetchAndRetry(url, init, callback) {
  38. callback !== null && callback !== void 0 ? callback : (callback = async (response) => response);
  39. return await semaphore.runExclusive(async () => {
  40. var _a;
  41. let delayResolver;
  42. let delayRejecter;
  43. try {
  44. // eslint-disable-next-line no-constant-condition
  45. for (let i = 0; true; i++) {
  46. const response = await fetch(url, init);
  47. if (response.ok) {
  48. const result = await callback(response, i);
  49. if (result !== undefined) {
  50. if (delayResolver !== undefined) {
  51. delayPromise = undefined;
  52. delayResolver();
  53. }
  54. return result;
  55. }
  56. }
  57. if (delayPromise === undefined) {
  58. delayPromise = new Promise((resolve, reject) => {
  59. delayResolver = resolve;
  60. delayRejecter = reject;
  61. });
  62. if (response.status === 429) { // Too Many Requests
  63. await delay(delayTooManyRequestsMs);
  64. }
  65. else {
  66. await delay(1000);
  67. }
  68. }
  69. else if (delayResolver !== undefined) {
  70. if (response.status === 429) { // Too Many Requests
  71. // retry after 10 seconds, then another 10 seconds, etc. up to 60 seconds, finally retry after every second.
  72. const additionalDelayMs = delayTooManyRequestsMs < 60000 ? 10000 : 1000;
  73. delayTooManyRequestsMs += additionalDelayMs;
  74. await delay(additionalDelayMs);
  75. }
  76. else if (i < 5) {
  77. // retry after 2 seconds, then 4 seconds, then 8, finally 16 (30 seconds total)
  78. await delay(Math.pow(2, i) * 1000);
  79. }
  80. else {
  81. throw (_a = response.statusText) !== null && _a !== void 0 ? _a : response.status.toString();
  82. }
  83. }
  84. else {
  85. await delayPromise;
  86. }
  87. }
  88. }
  89. catch (reason) {
  90. if (delayRejecter !== undefined) {
  91. delayRejecter(reason);
  92. }
  93. throw reason;
  94. }
  95. });
  96. }
  97.  
  98.  
  99. /***/ }),
  100.  
  101. /***/ 156:
  102. /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
  103.  
  104. "use strict";
  105.  
  106. var __importDefault = (this && this.__importDefault) || function (mod) {
  107. return (mod && mod.__esModule) ? mod : { "default": mod };
  108. };
  109. Object.defineProperty(exports, "__esModule", ({ value: true }));
  110. const he_1 = __importDefault(__webpack_require__(488));
  111. const constants_1 = __webpack_require__(921);
  112. const create_timestamp_links_1 = __webpack_require__(641);
  113. const display_album_name_1 = __webpack_require__(308);
  114. const enhance_automatic_edits_page_1 = __webpack_require__(252);
  115. const LoadingModal_1 = __webpack_require__(694);
  116. const Modal_1 = __webpack_require__(946);
  117. const utils_1 = __webpack_require__(135);
  118. // use the top-right link to determine the current user
  119. const authLink = document.querySelector('a.auth-link');
  120. // https://regex101.com/r/UCmC8f/1
  121. const albumRegExp = new RegExp(`^${authLink === null || authLink === void 0 ? void 0 : authLink.href}/library/music(/\\+[^/]*)*(/[^+][^/]*){2}$`);
  122. const artistRegExp = new RegExp(`^${authLink === null || authLink === void 0 ? void 0 : authLink.href}/library/music(/\\+[^/]*)*(/[^+][^/]*){1}(/\\+[^/]*)?$`);
  123. const domParser = new DOMParser();
  124. const bulkEditScrobbleFormTemplate = document.createElement('template');
  125. bulkEditScrobbleFormTemplate.innerHTML = `
  126. <form method="POST" action="${authLink === null || authLink === void 0 ? void 0 : authLink.getAttribute('href')}/library/edit?edited-variation=library-track-scrobble"
  127. data-edit-scrobble data-bulk-edit-scrobbles>
  128. <input type="hidden" name="csrfmiddlewaretoken" value="">
  129. <input type="hidden" name="artist_name" value="">
  130. <input type="hidden" name="track_name" value="">
  131. <input type="hidden" name="album_name" value="">
  132. <input type="hidden" name="album_artist_name" value="">
  133. <input type="hidden" name="timestamp" value="">
  134. <button type="submit" style="display: none;"></button>
  135. </form>`;
  136. if (authLink) {
  137. initialize();
  138. }
  139. function initialize() {
  140. appendStyle();
  141. appendBulkEditScrobblesHeaderLinkAndMenuItems(document.body);
  142. (0, create_timestamp_links_1.createTimestampLinks)(document.body);
  143. (0, display_album_name_1.displayAlbumName)(document.body);
  144. (0, enhance_automatic_edits_page_1.enhanceAutomaticEditsPage)(document.body);
  145. // use MutationObserver because Last.fm is a single-page application
  146. const observer = new MutationObserver((mutations) => {
  147. for (const mutation of mutations) {
  148. for (const node of mutation.addedNodes) {
  149. if (node instanceof Element) {
  150. if (node.hasAttribute('data-processed')) {
  151. continue;
  152. }
  153. node.setAttribute('data-processed', 'true');
  154. appendBulkEditScrobblesHeaderLinkAndMenuItems(node);
  155. (0, create_timestamp_links_1.createTimestampLinks)(document.body);
  156. (0, display_album_name_1.displayAlbumName)(node);
  157. (0, enhance_automatic_edits_page_1.enhanceAutomaticEditsPage)(node);
  158. }
  159. }
  160. }
  161. });
  162. observer.observe(document.body, {
  163. childList: true,
  164. subtree: true,
  165. });
  166. }
  167. function appendStyle() {
  168. const style = document.createElement('style');
  169. style.innerHTML = `
  170. .${constants_1.namespace}-title[title] {
  171. cursor: help !important;
  172. }
  173.  
  174. @media (pointer: coarse), (hover: none) {
  175. .${constants_1.namespace}-title[title]:focus {
  176. position: relative;
  177. display: inline-flex;
  178. justify-content: center;
  179. }
  180.  
  181. .${constants_1.namespace}-title[title]:focus::after {
  182. content: attr(title);
  183. position: absolute;
  184. top: 100%;
  185. left: 0%;
  186. color: #fff;
  187. background-color: #2b2a32;
  188. border: 1px solid #fff;
  189. width: fit-content;
  190. padding: 4px 7px;
  191. font-size: small;
  192. line-height: normal;
  193. white-space: pre;
  194. z-index: 1;
  195. }
  196. }
  197.  
  198. .${constants_1.namespace}-ellipsis {
  199. display: block;
  200. overflow: hidden;
  201. text-overflow: ellipsis;
  202. white-space: nowrap;
  203. }
  204.  
  205. .${constants_1.namespace}-form-group-controls {
  206. margin-left: 0 !important;
  207. }
  208.  
  209. .${constants_1.namespace}-list {
  210. column-count: 2;
  211. }
  212.  
  213. .${constants_1.namespace}-loading {
  214. background: url("/static/images/loading_dark_light_64.gif") 50% 50% no-repeat;
  215. height: 64px;
  216. display: flex;
  217. justify-content: center;
  218. align-items: center;
  219. }
  220.  
  221. .${constants_1.namespace}-text-danger {
  222. color: #d92323;
  223. }
  224.  
  225. .${constants_1.namespace}-text-info {
  226. color: #2b65d9;
  227. }
  228.  
  229. @media (min-width: 768px) {
  230. .${constants_1.namespace}-chartlist-scrobbles .chartlist-name {
  231. margin-top: -2px;
  232. margin-bottom: 13px;
  233. }
  234.  
  235. .${constants_1.namespace}-chartlist-scrobbles .chartlist-album {
  236. margin-top: 13px;
  237. margin-bottom: -2px;
  238. position: absolute;
  239. left: 133.5px;
  240. width: 182.41px;
  241. }
  242.  
  243. .${constants_1.namespace}-chartlist-scrobbles .chartlist-album::before {
  244. width: 0 !important;
  245. }
  246. }
  247.  
  248. @media (min-width: 1260px) {
  249. .${constants_1.namespace}-chartlist-scrobbles .chartlist-album {
  250. width: 272.41px;
  251. }
  252. }
  253.  
  254. .${constants_1.namespace}-highlight {
  255. background-color: #fff9e5;
  256. }
  257.  
  258. .${constants_1.namespace}-highlight:hover {
  259. background-color: #fcf2cf !important;
  260. }`;
  261. document.head.appendChild(style);
  262. }
  263. function appendBulkEditScrobblesHeaderLinkAndMenuItems(element) {
  264. if (!document.URL.startsWith(authLink.href)) {
  265. return; // current page is not the user's profile
  266. }
  267. appendBulkEditScrobblesHeaderLink(element);
  268. appendBulkEditScrobblesMenuItems(element);
  269. }
  270. function appendBulkEditScrobblesHeaderLink(element) {
  271. var _a;
  272. const header = element.querySelector('.library-header');
  273. if (header === null) {
  274. return; // current page does not contain the header we're looking for
  275. }
  276. const { form, click } = getBulkEditScrobbleMenuItem(document.URL);
  277. const link = document.createElement('a');
  278. link.href = 'javascript:void(0)';
  279. link.textContent = 'Bulk edit scrobbles';
  280. link.addEventListener('click', click);
  281. if (((_a = header.lastElementChild) === null || _a === void 0 ? void 0 : _a.tagName) !== 'H2') {
  282. header.insertAdjacentText('beforeend', ' · ');
  283. }
  284. header.insertAdjacentElement('beforeend', link);
  285. header.insertAdjacentElement('beforeend', form);
  286. }
  287. function appendBulkEditScrobblesMenuItems(element) {
  288. const rows = element instanceof HTMLTableRowElement ? [element] : element.querySelectorAll('tr');
  289. for (const row of rows) {
  290. const link = row.querySelector('a.chartlist-count-bar-link,a.more-item--track[href*="/user/"]');
  291. if (!link) {
  292. continue; // this is not an artist, album or track
  293. }
  294. const { form, click } = getBulkEditScrobbleMenuItem(link.href, row);
  295. const button = document.createElement('button');
  296. button.className = 'mimic-link dropdown-menu-clickable-item more-item--edit-old';
  297. button.textContent = 'Bulk edit scrobbles';
  298. button.setAttribute('data-analytics-action', 'BulkEditScrobblesOpen');
  299. button.addEventListener('click', click);
  300. form.style.marginTop = '0';
  301. const bulkEditScrobbleMenuItem = document.createElement('li');
  302. bulkEditScrobbleMenuItem.appendChild(button);
  303. bulkEditScrobbleMenuItem.appendChild(form);
  304. bulkEditScrobbleMenuItem.setAttribute('data-processed', 'true');
  305. // insert/replace "Bulk edit scrobbles" menu item so it comes after "Edit scrobble"
  306. const menu = row.querySelector('.chartlist-more-menu');
  307. let editScrobbleMenuItem = undefined;
  308. for (const menuItem of menu.children) {
  309. if (menuItem.hasAttribute('data-processed')) {
  310. menu.removeChild(menuItem);
  311. }
  312. else if (menuItem.querySelector('button.more-item--edit-old') !== null) {
  313. editScrobbleMenuItem = menuItem;
  314. }
  315. }
  316. if (editScrobbleMenuItem) {
  317. menu.insertBefore(bulkEditScrobbleMenuItem, editScrobbleMenuItem.nextElementSibling);
  318. }
  319. else {
  320. menu.insertBefore(bulkEditScrobbleMenuItem, menu.firstElementChild);
  321. }
  322. }
  323. }
  324. function getBulkEditScrobbleMenuItem(url, row) {
  325. const urlType = getUrlType(url);
  326. const form = bulkEditScrobbleFormTemplate.content.firstElementChild.cloneNode(true);
  327. const submitButton = form.querySelector('button');
  328. let allScrobbleData;
  329. let scrobbleData;
  330. const click = async () => {
  331. if (!allScrobbleData) {
  332. const loadingModal = createLoadingModal('Loading Scrobbles...', { dismissible: true, display: 'percentage' });
  333. try {
  334. allScrobbleData = await fetchScrobbleData(url, loadingModal, loadingModal);
  335. if (!loadingModal.isAttached) {
  336. return;
  337. }
  338. }
  339. finally {
  340. loadingModal.hide();
  341. }
  342. }
  343. scrobbleData = allScrobbleData;
  344. // use JSON strings as album keys to uniquely identify combinations of album + album artists
  345. // group scrobbles by album key
  346. let scrobbleDataGroups = [...groupBy(allScrobbleData, (s) => {
  347. var _a, _b;
  348. return JSON.stringify({
  349. album_name: (_a = s.get('album_name')) !== null && _a !== void 0 ? _a : '',
  350. album_artist_name: (_b = s.get('album_artist_name')) !== null && _b !== void 0 ? _b : '',
  351. });
  352. })];
  353. // sort groups by the amount of scrobbles
  354. scrobbleDataGroups = scrobbleDataGroups.sort(([_key1, values1], [_key2, values2]) => values2.length - values1.length);
  355. // when editing multiple albums album, show an album selection dialog first
  356. if (scrobbleDataGroups.length >= 2) {
  357. const noAlbumKey = JSON.stringify({ album_name: '', album_artist_name: '' });
  358. let currentAlbumKey = undefined;
  359. // put the "No Album" album first
  360. scrobbleDataGroups = scrobbleDataGroups.sort(([key1], [key2]) => {
  361. if (key1 === noAlbumKey)
  362. return -1;
  363. if (key2 === noAlbumKey)
  364. return +1;
  365. return 0;
  366. });
  367. // when the edit dialog was initiated from an album or album track, put that album first in the list
  368. if (urlType === 'album' || getUrlType(document.URL) === 'album') {
  369. // grab the current album name and artist name from the DOM
  370. const album_name = (urlType === 'album' && row
  371. ? row.querySelector('.chartlist-name')
  372. : document.querySelector('.library-header-title')).textContent.trim();
  373. const album_artist_name = (urlType === 'album' && row
  374. ? row.querySelector('.chartlist-artist') || document.querySelector('.library-header-title, .library-header-crumb')
  375. : document.querySelector('.text-colour-link')).textContent.trim();
  376. currentAlbumKey = JSON.stringify({ album_name, album_artist_name });
  377. // put the current album first
  378. scrobbleDataGroups = scrobbleDataGroups.sort(([key1], [key2]) => {
  379. if (key1 === currentAlbumKey)
  380. return -1;
  381. if (key2 === currentAlbumKey)
  382. return +1;
  383. if (key1 === noAlbumKey)
  384. return -1;
  385. if (key2 === noAlbumKey)
  386. return +1;
  387. return 0;
  388. });
  389. }
  390. const body = document.createElement('div');
  391. body.innerHTML = `
  392. <div class="form-disclaimer">
  393. <div class="alert alert-info">
  394. ${urlType === 'track' ? 'This track is' : `Tracks from this ${urlType} are`} scrobbled under multiple albums.
  395. Select which albums you would like to edit.
  396. Deselect albums you would like to skip.
  397. </div>
  398. </div>
  399. <div class="form-group">
  400. <div class="form-group-controls ${constants_1.namespace}-form-group-controls">
  401. <button type="button" class="btn-secondary" id="${constants_1.namespace}-select-all">Select all</button>
  402. <button type="button" class="btn-secondary" id="${constants_1.namespace}-deselect-all">Deselect all</button>
  403. </div>
  404. </div>
  405. <ul class="${constants_1.namespace}-list">
  406. ${scrobbleDataGroups.map(([key, scrobbleData]) => {
  407. var _a;
  408. const firstScrobbleData = scrobbleData[0];
  409. const album_name = firstScrobbleData.get('album_name');
  410. const artist_name = ((_a = firstScrobbleData.get('album_artist_name')) !== null && _a !== void 0 ? _a : firstScrobbleData.get('artist_name'));
  411. return `
  412. <li>
  413. <div class="checkbox">
  414. <label>
  415. <input type="checkbox" name="key" value="${he_1.default.escape(key)}" ${currentAlbumKey === undefined || currentAlbumKey === key ? 'checked' : ''} />
  416. <strong title="${he_1.default.escape(album_name !== null && album_name !== void 0 ? album_name : '')}" class="${constants_1.namespace}-ellipsis ${currentAlbumKey === key ? `${constants_1.namespace}-text-info` : !album_name ? `${constants_1.namespace}-text-danger` : ''}">
  417. ${album_name ? he_1.default.escape(album_name) : '<em>No Album</em>'}
  418. </strong>
  419. <div title="${he_1.default.escape(artist_name)}" class="${constants_1.namespace}-ellipsis">
  420. ${he_1.default.escape(artist_name)}
  421. </div>
  422. <small>
  423. ${scrobbleData.length} scrobble${scrobbleData.length !== 1 ? 's' : ''}
  424. </small>
  425. </label>
  426. </div>
  427. </li>`;
  428. }).join('')}
  429. </ul>`;
  430. const checkboxes = body.querySelectorAll('input[type="checkbox"]');
  431. body.querySelector(`#${constants_1.namespace}-select-all`).addEventListener('click', () => {
  432. for (const checkbox of checkboxes) {
  433. checkbox.checked = true;
  434. }
  435. });
  436. body.querySelector(`#${constants_1.namespace}-deselect-all`).addEventListener('click', () => {
  437. for (const checkbox of checkboxes) {
  438. checkbox.checked = false;
  439. }
  440. });
  441. let formData;
  442. try {
  443. formData = await prompt('Select Albums To Edit', body);
  444. }
  445. catch (error) {
  446. return; // user canceled the album selection dialog
  447. }
  448. const selectedAlbumKeys = formData.getAll('key');
  449. scrobbleData = scrobbleDataGroups
  450. .filter(([key]) => selectedAlbumKeys.includes(key))
  451. .map(([_, values]) => values)
  452. .flat();
  453. }
  454. if (scrobbleData.length === 0) {
  455. alert(`Last.fm reports you haven't listened to this ${urlType}.`);
  456. return;
  457. }
  458. // use the first scrobble to trick Last.fm into fetching the Edit Scrobble modal
  459. applyFormData(form, scrobbleData[0]);
  460. submitButton.click();
  461. };
  462. submitButton.addEventListener('click', async () => {
  463. await augmentEditScrobbleForm(scrobbleData);
  464. });
  465. return { form, click };
  466. }
  467. // shows a form dialog and resolves its promise on submit
  468. function prompt(title, body) {
  469. return new Promise((resolve, reject) => {
  470. const form = document.createElement('form');
  471. form.className = 'form-horizontal';
  472. if (body instanceof Element) {
  473. form.insertAdjacentElement('beforeend', body);
  474. }
  475. else {
  476. form.insertAdjacentHTML('beforeend', body);
  477. }
  478. form.insertAdjacentHTML('beforeend', `
  479. <div class="form-group form-group--submit">
  480. <div class="form-submit">
  481. <button type="reset" class="btn-secondary">Cancel</button>
  482. <button type="submit" class="btn-primary">
  483. <span class="btn-inner">
  484. OK
  485. </span>
  486. </button>
  487. </div>
  488. </div>`);
  489. const content = document.createElement('div');
  490. content.className = 'content-form';
  491. content.appendChild(form);
  492. const modal = new Modal_1.Modal(title, content, {
  493. dismissible: true,
  494. events: {
  495. hide: reject,
  496. },
  497. });
  498. form.addEventListener('reset', () => modal.hide());
  499. form.addEventListener('submit', (event) => {
  500. event.preventDefault();
  501. resolve(new FormData(form));
  502. modal.hide();
  503. });
  504. modal.show();
  505. });
  506. }
  507. function createLoadingModal(title, options) {
  508. const modal = new LoadingModal_1.LoadingModal(title, options);
  509. modal.show();
  510. return modal;
  511. }
  512. // this is a recursive function that browses pages of artists, albums and tracks to gather scrobbles
  513. async function fetchScrobbleData(url, loadingModal, parentStep) {
  514. // remove "?date_preset=LAST_365_DAYS", etc.
  515. const indexOfQuery = url.indexOf('?');
  516. if (indexOfQuery !== -1) {
  517. url = url.substring(0, indexOfQuery);
  518. }
  519. switch (getUrlType(url)) {
  520. case 'artist':
  521. if (!url.endsWith('/+tracks')) {
  522. url += '/+tracks'; // skip artist overview and go straight to the tracks
  523. }
  524. break;
  525. case 'track':
  526. if (!url.includes('/library/music/+noredirect/')) {
  527. url = url.replace('/library/music/', '/library/music/+noredirect/'); // avoid redirects
  528. }
  529. break;
  530. }
  531. const documentsToFetch = [fetchHTMLDocument(url)];
  532. const firstDocument = await documentsToFetch[0];
  533. const paginationList = firstDocument.querySelector('.pagination-list');
  534. if (paginationList) {
  535. const pageCount = parseInt(paginationList.children[paginationList.children.length - 2].textContent.trim(), 10);
  536. const pageNumbersToFetch = [...Array(pageCount - 1).keys()].map((i) => i + 2);
  537. documentsToFetch.push(...pageNumbersToFetch.map((n) => fetchHTMLDocument(`${url}?page=${n}`)));
  538. }
  539. const scrobbleData = await forEachParallel(loadingModal, parentStep, documentsToFetch, async (documentToFetch, step) => {
  540. const fetchedDocument = await documentToFetch;
  541. const table = fetchedDocument.querySelector('table.chartlist:not(.chartlist__placeholder)');
  542. if (!table) {
  543. // sometimes a missing chartlist is expected, other times it indicates a failure
  544. if (fetchedDocument.body.textContent.includes('There was a problem loading your')) {
  545. abort('There was a problem loading your scrobbles, please try again later.');
  546. }
  547. return [];
  548. }
  549. const rows = [...table.tBodies[0].rows];
  550. // to display accurate loading percentages, tracks with more scrobbles will have more weight
  551. const weightFunc = (row) => {
  552. const barValue = row.querySelector('.chartlist-count-bar-value');
  553. if (barValue === null)
  554. return 1;
  555. const scrobbleCount = parseInt(barValue.firstChild.textContent.trim().replace(/,/g, ''), 10);
  556. return Math.ceil(scrobbleCount / 50); // 50 = items per page on Last.fm
  557. };
  558. const scrobbleData = await forEachParallel(loadingModal, step, rows, async (row, step) => {
  559. const link = row.querySelector('.chartlist-count-bar-link');
  560. if (link) {
  561. // recursive call to the current function
  562. return await fetchScrobbleData(link.href, loadingModal, step);
  563. }
  564. // no link indicates we're at the scrobble overview
  565. const form = row.querySelector('form[data-edit-scrobble]');
  566. return [new FormData(form)];
  567. }, weightFunc);
  568. return scrobbleData.flat();
  569. });
  570. return scrobbleData.flat();
  571. }
  572. function getUrlType(url) {
  573. if (albumRegExp.test(url)) {
  574. return 'album';
  575. }
  576. else if (artistRegExp.test(url)) {
  577. if (url.endsWith('/+albums')) {
  578. return 'album artist';
  579. }
  580. else {
  581. return 'artist';
  582. }
  583. }
  584. else {
  585. return 'track';
  586. }
  587. }
  588. async function fetchHTMLDocument(url) {
  589. try {
  590. return await (0, utils_1.fetchAndRetry)(url, undefined, async (response, i) => {
  591. const html = await response.text();
  592. const doc = domParser.parseFromString(html, 'text/html');
  593. if (doc.querySelector('table.chartlist:not(.chartlist__placeholder)') || i >= 5) {
  594. return doc;
  595. }
  596. });
  597. }
  598. catch (error) {
  599. const message = `There was a problem loading your scrobbles, please try again later. (${error})`;
  600. abort(message);
  601. throw message;
  602. }
  603. }
  604. let aborting = false;
  605. function abort(message) {
  606. if (aborting)
  607. return;
  608. aborting = true;
  609. alert(message);
  610. window.location.reload();
  611. }
  612. // series for loop that updates the loading percentage
  613. async function forEach(loadingModal, parentStep, array, callback, weightFunc) {
  614. const tuples = array.map((item) => ({ item, step: { completed: false, steps: [], weight: weightFunc ? weightFunc(item) : 1 } }));
  615. parentStep.steps.push(...tuples.map((tuple) => tuple.step));
  616. loadingModal.refreshProgress();
  617. const result = [];
  618. for (const tuple of tuples) {
  619. result.push(await callback(tuple.item, tuple.step));
  620. tuple.step.completed = true;
  621. loadingModal.refreshProgress();
  622. }
  623. return result.flat();
  624. }
  625. // parallel for loop that updates the loading percentage
  626. function forEachParallel(loadingModal, parentStep, array, callback, weightFunc) {
  627. const tuples = array.map((item) => ({ item, step: { completed: false, steps: [], weight: weightFunc ? weightFunc(item) : 1 } }));
  628. parentStep.steps.push(...tuples.map((tuple) => tuple.step));
  629. loadingModal.refreshProgress();
  630. return Promise.all(tuples.map(async (tuple) => {
  631. const result = await callback(tuple.item, tuple.step);
  632. tuple.step.completed = true;
  633. loadingModal.refreshProgress();
  634. return result;
  635. }));
  636. }
  637. function applyFormData(form, formData) {
  638. for (const [name, value] of formData) {
  639. const input = form.querySelector(`input[name="${name}"]`);
  640. input.value = value;
  641. }
  642. }
  643. // augments the default Edit Scrobble form to include new features
  644. async function augmentEditScrobbleForm(scrobbleData) {
  645. const loadingModal = createLoadingModal('Waiting for Last.fm...', { dismissible: true });
  646. let popup;
  647. try {
  648. popup = await observeChildList(document.body, '.popup_content');
  649. }
  650. finally {
  651. loadingModal.hide();
  652. }
  653. const title = popup.querySelector('.modal-title');
  654. const form = popup.querySelector('form[action$="/library/edit?edited-variation=library-track-scrobble"]');
  655. const elements = form.elements;
  656. title.textContent = `Bulk Edit Scrobbles`;
  657. // remove traces of the first scrobble that was used to initialize the form
  658. const topBox = form.querySelector('.edit-scrobble-top-box');
  659. if (topBox) {
  660. form.removeChild(topBox);
  661. }
  662. const track_name_input = elements.track_name;
  663. const artist_name_input = elements.artist_name;
  664. const album_name_input = elements.album_name;
  665. const album_artist_name_input = elements.album_artist_name;
  666. const tracks = augmentInput(scrobbleData, popup, elements, elements.track_name_original, track_name_input, 'tracks');
  667. augmentInput(scrobbleData, popup, elements, elements.artist_name_original, artist_name_input, 'artists');
  668. augmentInput(scrobbleData, popup, elements, elements.album_name_original, album_name_input, 'albums');
  669. augmentInput(scrobbleData, popup, elements, elements.album_artist_name_original, album_artist_name_input, 'album artists');
  670. // add information alert about album artists being kept in sync
  671. if (album_artist_name_input.placeholder === 'Mixed' && scrobbleData.some((s) => s.get('album_artist_name') === artist_name_input.value)) {
  672. const messageTemplate = document.createElement('template');
  673. messageTemplate.innerHTML = `
  674. <div class="form-group-success">
  675. <div class="alert alert-info">
  676. <p>Matching album artists will be kept in sync.</p>
  677. </div>
  678. </div>`;
  679. const message = messageTemplate.content.firstElementChild.cloneNode(true);
  680. const formGroup = album_artist_name_input.parentElement;
  681. formGroup.parentElement.insertBefore(message, formGroup.nextElementSibling.nextElementSibling);
  682. const removeMessage = () => {
  683. message.parentElement.removeChild(message);
  684. album_artist_name_input.removeEventListener('input', removeMessage);
  685. album_artist_name_input.removeEventListener('keydown', removeMessage);
  686. };
  687. album_artist_name_input.addEventListener('input', removeMessage);
  688. album_artist_name_input.addEventListener('keydown', removeMessage);
  689. }
  690. // keep album artist name in sync
  691. let previousValue = artist_name_input.value;
  692. artist_name_input.addEventListener('input', () => {
  693. if (album_artist_name_input.value === previousValue && album_artist_name_input.placeholder !== 'Mixed') {
  694. album_artist_name_input.value = artist_name_input.value;
  695. album_artist_name_input.dispatchEvent(new Event('input'));
  696. }
  697. previousValue = artist_name_input.value;
  698. });
  699. // update the "Bulk edit" checkbox
  700. if (elements.edit_all) {
  701. elements.edit_all.checked = true;
  702. elements.edit_all.disabled = true;
  703. elements.edit_all.parentElement.style.cursor = 'auto';
  704. elements.edit_all.nextSibling.textContent = tracks > 1
  705. ? `Apply to all (${scrobbleData.length}) past scrobbles of ${tracks} tracks`
  706. : elements.edit_all.nextSibling.textContent.replace(/\d+/, scrobbleData.length.toString());
  707. const hiddenInput = document.createElement('input');
  708. hiddenInput.type = 'hidden';
  709. hiddenInput.name = elements.edit_all.name;
  710. hiddenInput.value = elements.edit_all.value;
  711. elements.edit_all.parentElement.insertBefore(hiddenInput, elements.edit_all.nextElementSibling);
  712. }
  713. // update the "Automatic edit" checkbox
  714. if (tracks > 1) {
  715. elements.create_automatic_edit_rule.nextSibling.textContent =
  716. `Apply to all future scrobbles of ${tracks} tracks`;
  717. }
  718. // each exact track, artist, album and album artist combination is considered a distinct scrobble
  719. const distinctGroups = groupBy(scrobbleData, (s) => {
  720. var _a, _b;
  721. return JSON.stringify({
  722. track_name: s.get('track_name'),
  723. artist_name: s.get('artist_name'),
  724. album_name: (_a = s.get('album_name')) !== null && _a !== void 0 ? _a : '',
  725. album_artist_name: (_b = s.get('album_artist_name')) !== null && _b !== void 0 ? _b : '',
  726. });
  727. });
  728. const distinctScrobbleData = [...distinctGroups].map(([_name, values]) => values[0]);
  729. // disable the submit button when the form has validation errors
  730. const submitButton = form.querySelector('button[type="submit"]');
  731. form.addEventListener('input', () => {
  732. submitButton.disabled = form.querySelector('.has-error') !== null;
  733. });
  734. // set up the form submit event listener
  735. submitButton.addEventListener('click', async (event) => {
  736. var _a, _b;
  737. event.preventDefault();
  738. const formData = new FormData(form);
  739. const formDataToSubmit = [];
  740. const track_name = getMixedInputValue(track_name_input);
  741. const artist_name = getMixedInputValue(artist_name_input);
  742. const album_name = getMixedInputValue(album_name_input);
  743. const album_artist_name = getMixedInputValue(album_artist_name_input);
  744. for (const originalData of distinctScrobbleData) {
  745. const track_name_original = originalData.get('track_name');
  746. const artist_name_original = originalData.get('artist_name');
  747. const album_name_original = (_a = originalData.get('album_name')) !== null && _a !== void 0 ? _a : '';
  748. const album_artist_name_original = (_b = originalData.get('album_artist_name')) !== null && _b !== void 0 ? _b : '';
  749. // if the album artist field is Mixed, use the old and new artist names to keep the album artist in sync
  750. const album_artist_name_sync = album_artist_name_input.placeholder === 'Mixed' && distinctScrobbleData.some((s) => s.get('artist_name') === album_artist_name_original)
  751. ? artist_name
  752. : album_artist_name;
  753. // check if anything changed compared to the original track, artist, album and album artist combination
  754. if (track_name !== null && track_name !== track_name_original ||
  755. artist_name !== null && artist_name !== artist_name_original ||
  756. album_name !== null && album_name !== album_name_original ||
  757. album_artist_name_sync !== null && album_artist_name_sync !== album_artist_name_original) {
  758. const clonedFormData = cloneFormData(formData);
  759. // Last.fm expects a timestamp
  760. clonedFormData.set('timestamp', originalData.get('timestamp'));
  761. // populate the *_original fields to instruct Last.fm which scrobbles need to be edited
  762. clonedFormData.set('track_name_original', track_name_original);
  763. if (track_name === null) {
  764. clonedFormData.set('track_name', track_name_original);
  765. }
  766. clonedFormData.set('artist_name_original', artist_name_original);
  767. if (artist_name === null) {
  768. clonedFormData.set('artist_name', artist_name_original);
  769. }
  770. clonedFormData.set('album_name_original', album_name_original);
  771. if (album_name === null) {
  772. clonedFormData.set('album_name', album_name_original);
  773. }
  774. clonedFormData.set('album_artist_name_original', album_artist_name_original);
  775. if (album_artist_name_sync === null) {
  776. clonedFormData.set('album_artist_name', album_artist_name_original);
  777. }
  778. else {
  779. clonedFormData.set('album_artist_name', album_artist_name_sync);
  780. }
  781. clonedFormData.set('ajax', '1');
  782. formDataToSubmit.push(clonedFormData);
  783. }
  784. }
  785. if (formDataToSubmit.length === 0) {
  786. alert('Your edit doesn\'t contain any real changes. We cannot accept casing changes.'); // TODO: pretty validation messages
  787. return;
  788. }
  789. if (formDataToSubmit.length > 1) {
  790. for (const element of form.elements) {
  791. if (element instanceof HTMLInputElement && element.dataset['confirm'] && element.placeholder !== 'Mixed') {
  792. if (!confirm(element.dataset['confirm'])) {
  793. return; // stop submit
  794. }
  795. }
  796. }
  797. }
  798. // hide the Edit Scrobble form
  799. const cancelButton = form.querySelector('button.js-close');
  800. cancelButton.click();
  801. const loadingModal = createLoadingModal('Saving Edits...', { dismissible: false, display: 'count' });
  802. const parentStep = loadingModal;
  803. // run edits in series, inconsistencies will arise if you use a parallel loop
  804. await forEach(loadingModal, parentStep, formDataToSubmit, async (formData) => {
  805. // Edge does not support passing formData into URLSearchParams() constructor
  806. const body = new URLSearchParams();
  807. for (const [name, value] of formData) {
  808. body.append(name, value);
  809. }
  810. const response = await (0, utils_1.fetchAndRetry)(form.action, { method: 'POST', body: body });
  811. const html = await response.text();
  812. // use DOMParser to check the response for alerts
  813. const placeholder = domParser.parseFromString(html, 'text/html');
  814. for (const message of placeholder.querySelectorAll('.alert-danger')) {
  815. alert(message.textContent.trim()); // TODO: pretty validation messages
  816. }
  817. });
  818. // Last.fm sometimes displays old data when reloading too fast, so wait 1 second
  819. setTimeout(() => { window.location.reload(); }, 1000);
  820. });
  821. }
  822. // helper function that completes when a matching element gets appended
  823. function observeChildList(target, selector) {
  824. return new Promise((resolve) => {
  825. const observer = new MutationObserver((mutations) => {
  826. for (const mutation of mutations) {
  827. for (const node of mutation.addedNodes) {
  828. if (node instanceof Element && node.matches(selector)) {
  829. observer.disconnect();
  830. resolve(node);
  831. return;
  832. }
  833. }
  834. }
  835. });
  836. observer.observe(target, { childList: true });
  837. });
  838. }
  839. // turns a normal input into an input that supports the "Mixed" state
  840. function augmentInput(scrobbleData, popup, inputs, originalInput, input, plural) {
  841. var _a;
  842. const formGroup = input.closest('.form-group');
  843. const groups = [...groupBy(scrobbleData, (s) => s.get(input.name))].sort((a, b) => b[1].length - a[1].length);
  844. if (groups.length >= 2) {
  845. // display the "Mixed" placeholder when there are two or more possible values
  846. originalInput.value = '';
  847. originalInput.placeholder = 'Mixed';
  848. input.value = '';
  849. input.placeholder = 'Mixed';
  850. // remove the "Originally" text that only shows on small screens
  851. let elementToRemove = formGroup.previousElementSibling;
  852. while (elementToRemove !== null) {
  853. if (elementToRemove.classList.contains('edit-scrobble-label--originally')) {
  854. elementToRemove.parentElement.removeChild(elementToRemove);
  855. break;
  856. }
  857. elementToRemove = elementToRemove.previousElementSibling;
  858. }
  859. // display informational element
  860. const maxFigureLength = groups[0][1].length.toString().length;
  861. const abbr = document.createElement('span');
  862. abbr.className = `abbr ${constants_1.namespace}-title`;
  863. abbr.tabIndex = -1;
  864. abbr.textContent = `${groups.length} ${plural}`;
  865. abbr.title = groups
  866. .map(([key, values]) => {
  867. const figureLength = values.length.toString().length;
  868. const figureSpaces = '\u2007'.repeat(maxFigureLength - figureLength);
  869. return `${figureSpaces}${values.length}x ${key !== null && key !== void 0 ? key : ''}`;
  870. })
  871. .join('\n');
  872. formGroup.parentElement.insertBefore(abbr, formGroup.nextElementSibling);
  873. input.dataset['confirm'] = `You are about to merge scrobbles for ${groups.length} ${plural}. This cannot be undone. Would you like to continue?`;
  874. // datalist: a native HTML5 autocomplete feature
  875. const datalist = document.createElement('datalist');
  876. datalist.id = `${constants_1.namespace}-${popup.id}-${input.name}-datalist`;
  877. for (const [value] of groups) {
  878. const option = document.createElement('option');
  879. option.value = (_a = value) !== null && _a !== void 0 ? _a : '';
  880. datalist.appendChild(option);
  881. }
  882. input.autocomplete = 'off';
  883. input.setAttribute('list', datalist.id);
  884. formGroup.insertBefore(datalist, input.nextElementSibling);
  885. }
  886. // display green color when field was edited, red if it's not allowed to be empty
  887. const defaultValue = input.value;
  888. input.addEventListener('input', () => {
  889. input.placeholder = ''; // removes "Mixed" state
  890. refreshFormGroupState();
  891. });
  892. input.addEventListener('keydown', (event) => {
  893. if (event.keyCode === 8 || event.keyCode === 46) { // backspace or delete
  894. input.placeholder = ''; // removes "Mixed" state
  895. refreshFormGroupState();
  896. }
  897. });
  898. if (input.name === 'album_name') {
  899. inputs.album_artist_name.addEventListener('input', () => {
  900. refreshFormGroupState();
  901. });
  902. }
  903. else if (input.name === 'album_artist_name') {
  904. inputs.album_name.addEventListener('input', () => {
  905. var _a;
  906. if (input.value === '' && inputs.album_name.value !== '') {
  907. const newValue = ((_a = scrobbleData
  908. .find(x => x.get('album_name') === inputs.album_name.value)) === null || _a === void 0 ? void 0 : _a.get('album_artist_name')) || inputs.artist_name.value;
  909. if (newValue) {
  910. input.value = newValue;
  911. input.dispatchEvent(new Event('input'));
  912. return;
  913. }
  914. }
  915. refreshFormGroupState();
  916. });
  917. }
  918. function refreshFormGroupState() {
  919. formGroup.classList.remove('has-error');
  920. formGroup.classList.remove('has-success');
  921. if (input.value === '' && input.placeholder === ''
  922. && (input.name === 'track_name'
  923. || input.name === 'artist_name'
  924. || input.name === 'album_name' && (inputs.album_artist_name.value !== '' || inputs.album_artist_name.placeholder === 'Mixed')
  925. || input.name === 'album_artist_name' && (inputs.album_name.value !== '' || inputs.album_name.placeholder === 'Mixed'))) {
  926. formGroup.classList.add('has-error');
  927. }
  928. else if (input.value !== defaultValue || groups.length >= 2 && input.placeholder === '') {
  929. formGroup.classList.add('has-success');
  930. }
  931. }
  932. return groups.length;
  933. }
  934. function groupBy(array, keyFunc) {
  935. const map = new Map();
  936. for (const item of array) {
  937. const key = keyFunc(item);
  938. const value = map.get(key);
  939. if (!value) {
  940. map.set(key, [item]);
  941. }
  942. else {
  943. value.push(item);
  944. }
  945. }
  946. return map;
  947. }
  948. function getMixedInputValue(input) {
  949. return input.placeholder !== 'Mixed' ? input.value : null;
  950. }
  951. function cloneFormData(formData) {
  952. const clonedFormData = new FormData();
  953. for (const [name, value] of formData) {
  954. clonedFormData.append(name, value);
  955. }
  956. return clonedFormData;
  957. }
  958.  
  959.  
  960. /***/ }),
  961.  
  962. /***/ 252:
  963. /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
  964.  
  965. "use strict";
  966.  
  967. var __importDefault = (this && this.__importDefault) || function (mod) {
  968. return (mod && mod.__esModule) ? mod : { "default": mod };
  969. };
  970. Object.defineProperty(exports, "__esModule", ({ value: true }));
  971. exports.enhanceAutomaticEditsPage = enhanceAutomaticEditsPage;
  972. const tiny_async_pool_1 = __importDefault(__webpack_require__(692));
  973. const constants_1 = __webpack_require__(921);
  974. const utils_1 = __webpack_require__(135);
  975. const toolbarTemplate = document.createElement('template');
  976. toolbarTemplate.innerHTML = `
  977. <div>
  978. <button type="button" class="btn-primary" disabled>
  979. View All At Once
  980. </button>
  981. Go to album artist: <select></select>
  982. </div>`;
  983. const domParser = new DOMParser();
  984. const artistMap = new Map();
  985. let artistSelect = undefined;
  986. let scrollArtistIntoView = false;
  987. let loadPagesPromise = undefined;
  988. let loadPagesProgressElement = undefined;
  989. async function enhanceAutomaticEditsPage(element) {
  990. if (!document.URL.includes('/settings/subscription/automatic-edits')) {
  991. return;
  992. }
  993. const section = element.querySelector('#subscription-corrections');
  994. const table = section === null || section === void 0 ? void 0 : section.querySelector('table');
  995. if (!section || !table) {
  996. return;
  997. }
  998. enhanceTable(table);
  999. const paginationList = section.querySelector('.pagination-list');
  1000. if (!paginationList) {
  1001. return;
  1002. }
  1003. const paginationListItems = [...paginationList.querySelectorAll('.pagination-page')];
  1004. const currentPageNumber = parseInt(paginationListItems.find(x => x.getAttribute('aria-current') === 'page').textContent, 10);
  1005. const pageCount = parseInt(paginationListItems[paginationListItems.length - 1].textContent, 10);
  1006. if (pageCount === 1) {
  1007. return;
  1008. }
  1009. const toolbar = toolbarTemplate.content.firstElementChild.cloneNode(true);
  1010. section.insertBefore(toolbar, section.firstElementChild);
  1011. artistSelect = toolbar.querySelector('select');
  1012. const selectedArtistKey = getSelectedArtistKey();
  1013. for (const artist of [...artistMap.values()].sort((a, b) => a.sortName.localeCompare(b.sortName))) {
  1014. const option = document.createElement('option');
  1015. option.value = artist.key;
  1016. option.selected = artist.key === selectedArtistKey;
  1017. option.text = artist.name;
  1018. const keepNothingSelected = !option.selected && artistSelect.selectedIndex === -1;
  1019. artistSelect.appendChild(option);
  1020. if (keepNothingSelected) {
  1021. artistSelect.selectedIndex = -1;
  1022. }
  1023. }
  1024. artistSelect.addEventListener('change', function () {
  1025. const selectedArtist = artistMap.get(this.value);
  1026. const anchor = document.createElement('a');
  1027. anchor.href = `?page=${selectedArtist.pageNumber}&album-artist=${(0, utils_1.encodeURIComponent2)(selectedArtist.name)}`;
  1028. document.body.appendChild(anchor);
  1029. scrollArtistIntoView = true;
  1030. anchor.click();
  1031. document.body.removeChild(anchor);
  1032. });
  1033. loadPagesProgressElement = document.createElement('span');
  1034. toolbar.insertAdjacentText('beforeend', ' ');
  1035. toolbar.insertAdjacentElement('beforeend', loadPagesProgressElement);
  1036. loadPagesPromise !== null && loadPagesPromise !== void 0 ? loadPagesPromise : (loadPagesPromise = loadPages(table, currentPageNumber, pageCount));
  1037. const pages = await loadPagesPromise;
  1038. toolbar.removeChild(loadPagesProgressElement);
  1039. const viewAllButton = toolbar.querySelector('button');
  1040. viewAllButton.disabled = false;
  1041. viewAllButton.addEventListener('click', async () => {
  1042. if (pages.length >= 10 && !window.confirm(`You are about to view ${pages.length} pages at once. This might take a long time to load. Are you sure?`)) {
  1043. return;
  1044. }
  1045. viewAllButton.disabled = true;
  1046. table.style.tableLayout = 'fixed';
  1047. const tableBody = table.tBodies[0];
  1048. const firstRow = tableBody.rows[0];
  1049. for (const page of pages) {
  1050. if (page.pageNumber === currentPageNumber) {
  1051. continue;
  1052. }
  1053. for (const row of page.rows) {
  1054. enhanceRow(row);
  1055. if (page.pageNumber < currentPageNumber) {
  1056. firstRow.insertAdjacentElement('beforebegin', row);
  1057. }
  1058. else {
  1059. tableBody.appendChild(row);
  1060. }
  1061. }
  1062. if (page.pageNumber % 10 === 0) {
  1063. await (0, utils_1.delay)(1);
  1064. }
  1065. }
  1066. });
  1067. }
  1068. function enhanceTable(table) {
  1069. document.body.style.backgroundColor = '#fff';
  1070. table.style.tableLayout = 'auto';
  1071. const headerRow = table.tHead.rows[0];
  1072. const body = table.tBodies[0];
  1073. let sortedCellIndex = 1;
  1074. const keys = [
  1075. 'track_name_original',
  1076. 'artist_name_original',
  1077. 'album_name_original',
  1078. 'album_artist_name_original',
  1079. ];
  1080. for (let i = 0; i < 4; i++) {
  1081. const key = keys[i];
  1082. const cell = headerRow.cells[i];
  1083. cell.innerHTML = `<a href="javascript:void(0)" role="button">${cell.textContent}</a>`;
  1084. cell.addEventListener('click', () => {
  1085. const dir = sortedCellIndex === i ? -1 : 1;
  1086. sortedCellIndex = sortedCellIndex === i ? -1 : i;
  1087. const rows = [...body.rows].map(row => {
  1088. let value = row.dataset[key];
  1089. if (!value) {
  1090. value = row.querySelector(`input[name="${key}"]`).value;
  1091. row.dataset[key] = value;
  1092. }
  1093. return { row, value };
  1094. });
  1095. rows.sort((a, b) => a.value.localeCompare(b.value) * dir);
  1096. for (const row of rows) {
  1097. body.appendChild(row.row);
  1098. }
  1099. });
  1100. }
  1101. for (const row of body.rows) {
  1102. enhanceRow(row);
  1103. }
  1104. }
  1105. function enhanceRow(row) {
  1106. if (row.dataset['enhanced'] === 'true') {
  1107. return;
  1108. }
  1109. row.dataset['enhanced'] = 'true';
  1110. const formData = getFormData(row);
  1111. const trackName = formData.get('track_name').toString();
  1112. const artistName = formData.get('artist_name').toString();
  1113. const albumName = formData.get('album_name').toString();
  1114. const albumArtistName = formData.get('album_artist_name').toString();
  1115. const originalTrackName = formData.get('track_name_original').toString();
  1116. const originalArtistName = formData.get('artist_name_original').toString();
  1117. const originalAlbumName = formData.get('album_name_original').toString();
  1118. const originalAlbumArtistName = formData.get('album_artist_name_original').toString();
  1119. function emphasize(cell, content) {
  1120. var _a;
  1121. cell.style.lineHeight = '1';
  1122. cell.innerHTML = `
  1123. <div>
  1124. <span class="sr-only">
  1125. ${cell.textContent}
  1126. </span>
  1127. <b>
  1128. ${content}
  1129. </b>
  1130. </div>
  1131. <small>
  1132. Originally "${(_a = cell.textContent) === null || _a === void 0 ? void 0 : _a.trim()}"
  1133. </small>`;
  1134. }
  1135. if (trackName !== originalTrackName) {
  1136. emphasize(row.cells[0], trackName);
  1137. }
  1138. else {
  1139. // remove bold
  1140. row.cells[0].innerHTML = row.cells[0].textContent;
  1141. }
  1142. if (artistName !== originalArtistName) {
  1143. emphasize(row.cells[1], artistName);
  1144. }
  1145. if (albumName !== originalAlbumName) {
  1146. emphasize(row.cells[2], albumName);
  1147. }
  1148. if (albumArtistName !== originalAlbumArtistName) {
  1149. emphasize(row.cells[3], albumArtistName);
  1150. }
  1151. if (originalAlbumArtistName.toLowerCase() === getSelectedArtistKey()) {
  1152. row.classList.add(`${constants_1.namespace}-highlight`);
  1153. if (scrollArtistIntoView) {
  1154. scrollArtistIntoView = false;
  1155. row.scrollIntoView({ behavior: 'smooth', block: 'start' });
  1156. }
  1157. }
  1158. }
  1159. function getFormData(row) {
  1160. return new FormData(row.querySelector('form'));
  1161. }
  1162. function getSelectedArtistKey() {
  1163. var _a;
  1164. return (_a = new URLSearchParams(location.search).get('album-artist')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
  1165. }
  1166. async function loadPages(table, currentPageNumber, pageCount) {
  1167. const currentPage = { pageNumber: currentPageNumber, rows: [...table.tBodies[0].rows] };
  1168. const pages = [currentPage];
  1169. const pageNumbersToLoad = [...Array(pageCount).keys()].map(i => i + 1).filter(i => i !== currentPageNumber);
  1170. addArtistsToSelect(currentPage);
  1171. updateProgressText(1, pageCount);
  1172. for await (const page of (0, tiny_async_pool_1.default)(6, pageNumbersToLoad, loadPage)) {
  1173. pages.push(page);
  1174. addArtistsToSelect(page);
  1175. updateProgressText(pages.length, pageCount);
  1176. }
  1177. pages.sort((a, b) => a.pageNumber < b.pageNumber ? -1 : 1);
  1178. return pages;
  1179. }
  1180. async function loadPage(pageNumber) {
  1181. const response = await (0, utils_1.fetchAndRetry)(`?page=${pageNumber}&_pjax=%23content`, {
  1182. credentials: 'include',
  1183. headers: {
  1184. 'X-Pjax': 'true',
  1185. 'X-Pjax-Container': '#content',
  1186. },
  1187. });
  1188. const text = await response.text();
  1189. const doc = domParser.parseFromString(text, 'text/html');
  1190. const table = doc.querySelector('.chart-table');
  1191. return {
  1192. pageNumber,
  1193. rows: [...table.tBodies[0].rows],
  1194. };
  1195. }
  1196. function addArtistsToSelect(page) {
  1197. const selectedArtistKey = getSelectedArtistKey();
  1198. for (const row of page.rows) {
  1199. const formData = getFormData(row);
  1200. const name = formData.get('album_artist_name_original').toString();
  1201. const sortName = name.replace(/\s+/g, '');
  1202. const key = name.toLowerCase();
  1203. const artist = artistMap.get(key);
  1204. if (!artist) {
  1205. artistMap.set(key, { key, name, sortName, pageNumber: page.pageNumber });
  1206. const option = document.createElement('option');
  1207. option.value = key;
  1208. option.selected = key === selectedArtistKey;
  1209. option.text = name;
  1210. const keepNothingSelected = !option.selected && artistSelect.selectedIndex === -1;
  1211. const insertAtIndex = [...artistMap.values()].sort((a, b) => a.sortName.localeCompare(b.sortName)).findIndex(x => x.key === key);
  1212. artistSelect.insertBefore(option, artistSelect.children[insertAtIndex]);
  1213. if (keepNothingSelected) {
  1214. artistSelect.selectedIndex = -1;
  1215. }
  1216. }
  1217. else if (artist.pageNumber > page.pageNumber) {
  1218. artist.pageNumber = page.pageNumber;
  1219. }
  1220. }
  1221. }
  1222. function updateProgressText(current, total) {
  1223. loadPagesProgressElement.textContent = `${current} / ${total} (${(current * 100 / total).toFixed(0)}%)`;
  1224. }
  1225.  
  1226.  
  1227. /***/ }),
  1228.  
  1229. /***/ 308:
  1230. /***/ ((__unused_webpack_module, exports) => {
  1231.  
  1232. "use strict";
  1233.  
  1234. Object.defineProperty(exports, "__esModule", ({ value: true }));
  1235. exports.displayAlbumName = displayAlbumName;
  1236. async function displayAlbumName(element) {
  1237. var _a, _b;
  1238. const rows = element instanceof HTMLTableRowElement ? [element] : element.querySelectorAll('tr');
  1239. if (rows.length === 0) {
  1240. return;
  1241. }
  1242. const baseHref = (_a = document.querySelector('.secondary-nav-item--overview a')) === null || _a === void 0 ? void 0 : _a.getAttribute('href');
  1243. for (const row of rows) {
  1244. // Ignore non-chartlist rows.
  1245. if (!row.matches('.chartlist-row[data-edit-scrobble-id]')) {
  1246. continue;
  1247. }
  1248. // Ignore non-chartlist tables and tables with an index.
  1249. const table = row.closest('table');
  1250. if (table === null || !table.matches('.chartlist:not(.chartlist--with-index)')) {
  1251. continue;
  1252. }
  1253. // Ignore rows without a cover art image or cover art placeholder.
  1254. const coverArtAnchor = row.querySelector('.cover-art');
  1255. if (coverArtAnchor === null) {
  1256. continue;
  1257. }
  1258. // Extract album link and name from cover art and scrobble edit form.
  1259. const albumHref = coverArtAnchor.getAttribute('href');
  1260. const form = row.querySelector('form[data-edit-scrobble]:not([data-bulk-edit-scrobbles])');
  1261. let albumName;
  1262. if (form !== null) {
  1263. const formData = new FormData(form);
  1264. albumName = (_b = formData.get('album_name')) === null || _b === void 0 ? void 0 : _b.toString();
  1265. }
  1266. else {
  1267. albumName = coverArtAnchor.querySelector('img').alt;
  1268. }
  1269. // Create and insert th element.
  1270. if (!table.classList.contains('lastfm-bulk-edit-chartlist-scrobbles')) {
  1271. table.classList.add('lastfm-bulk-edit-chartlist-scrobbles');
  1272. const albumHeaderCell = document.createElement('th');
  1273. albumHeaderCell.textContent = 'Album';
  1274. const headerRow = table.tHead.rows[0];
  1275. headerRow.insertBefore(albumHeaderCell, headerRow.children[4]);
  1276. }
  1277. // Create and insert td element.
  1278. const albumCell = document.createElement('td');
  1279. albumCell.className = 'chartlist-album';
  1280. if (albumHref && albumName) {
  1281. const albumAnchor = document.createElement('a');
  1282. albumAnchor.href = albumHref;
  1283. albumAnchor.title = albumName;
  1284. albumAnchor.textContent = albumName;
  1285. albumCell.appendChild(albumAnchor);
  1286. }
  1287. else {
  1288. const noAlbumText = document.createElement('em');
  1289. noAlbumText.className = 'lastfm-bulk-edit-text-danger';
  1290. noAlbumText.textContent = 'No Album';
  1291. albumCell.appendChild(noAlbumText);
  1292. }
  1293. const nameCell = row.querySelector('.chartlist-name');
  1294. row.insertBefore(albumCell, nameCell.nextElementSibling);
  1295. // Add menu items.
  1296. if (albumHref && albumName) {
  1297. const menu = row.querySelector('.chartlist-more-menu');
  1298. const albumMenuItem1 = document.createElement('li');
  1299. const menuItemAnchor1 = document.createElement('a');
  1300. menuItemAnchor1.href = albumHref;
  1301. menuItemAnchor1.className = 'dropdown-menu-clickable-item more-item--album';
  1302. menuItemAnchor1.textContent = 'Go to album';
  1303. albumMenuItem1.appendChild(menuItemAnchor1);
  1304. const albumMenuItem2 = document.createElement('li');
  1305. const menuItemAnchor2 = document.createElement('a');
  1306. menuItemAnchor2.href = baseHref + '/library' + albumHref;
  1307. menuItemAnchor2.className = 'dropdown-menu-clickable-item more-item--album';
  1308. menuItemAnchor2.textContent = 'Go to album in library';
  1309. albumMenuItem2.appendChild(menuItemAnchor2);
  1310. const artistMenuItem = menu.querySelector('.more-item--artist').parentNode;
  1311. menu.insertBefore(albumMenuItem1, artistMenuItem);
  1312. menu.insertBefore(albumMenuItem2, artistMenuItem);
  1313. }
  1314. }
  1315. }
  1316.  
  1317.  
  1318. /***/ }),
  1319.  
  1320. /***/ 406:
  1321. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  1322.  
  1323. "use strict";
  1324.  
  1325. Object.defineProperty(exports, "__esModule", ({ value: true }));
  1326. var tslib_1 = __webpack_require__(635);
  1327. var Semaphore_1 = __webpack_require__(919);
  1328. var Mutex = /** @class */ (function () {
  1329. function Mutex(cancelError) {
  1330. this._semaphore = new Semaphore_1.default(1, cancelError);
  1331. }
  1332. Mutex.prototype.acquire = function () {
  1333. return tslib_1.__awaiter(this, arguments, void 0, function (priority) {
  1334. var _a, releaser;
  1335. if (priority === void 0) { priority = 0; }
  1336. return tslib_1.__generator(this, function (_b) {
  1337. switch (_b.label) {
  1338. case 0: return [4 /*yield*/, this._semaphore.acquire(1, priority)];
  1339. case 1:
  1340. _a = _b.sent(), releaser = _a[1];
  1341. return [2 /*return*/, releaser];
  1342. }
  1343. });
  1344. });
  1345. };
  1346. Mutex.prototype.runExclusive = function (callback, priority) {
  1347. if (priority === void 0) { priority = 0; }
  1348. return this._semaphore.runExclusive(function () { return callback(); }, 1, priority);
  1349. };
  1350. Mutex.prototype.isLocked = function () {
  1351. return this._semaphore.isLocked();
  1352. };
  1353. Mutex.prototype.waitForUnlock = function (priority) {
  1354. if (priority === void 0) { priority = 0; }
  1355. return this._semaphore.waitForUnlock(1, priority);
  1356. };
  1357. Mutex.prototype.release = function () {
  1358. if (this._semaphore.isLocked())
  1359. this._semaphore.release();
  1360. };
  1361. Mutex.prototype.cancel = function () {
  1362. return this._semaphore.cancel();
  1363. };
  1364. return Mutex;
  1365. }());
  1366. exports["default"] = Mutex;
  1367.  
  1368.  
  1369. /***/ }),
  1370.  
  1371. /***/ 488:
  1372. /***/ ((module) => {
  1373.  
  1374. "use strict";
  1375. module.exports = he;
  1376.  
  1377. /***/ }),
  1378.  
  1379. /***/ 586:
  1380. /***/ ((__unused_webpack_module, exports) => {
  1381.  
  1382. "use strict";
  1383.  
  1384. Object.defineProperty(exports, "__esModule", ({ value: true }));
  1385. exports.E_CANCELED = exports.E_ALREADY_LOCKED = exports.E_TIMEOUT = void 0;
  1386. exports.E_TIMEOUT = new Error('timeout while waiting for mutex to become available');
  1387. exports.E_ALREADY_LOCKED = new Error('mutex already locked');
  1388. exports.E_CANCELED = new Error('request for lock canceled');
  1389.  
  1390.  
  1391. /***/ }),
  1392.  
  1393. /***/ 635:
  1394. /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
  1395.  
  1396. "use strict";
  1397. __webpack_require__.r(__webpack_exports__);
  1398. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  1399. /* harmony export */ __addDisposableResource: () => (/* binding */ __addDisposableResource),
  1400. /* harmony export */ __assign: () => (/* binding */ __assign),
  1401. /* harmony export */ __asyncDelegator: () => (/* binding */ __asyncDelegator),
  1402. /* harmony export */ __asyncGenerator: () => (/* binding */ __asyncGenerator),
  1403. /* harmony export */ __asyncValues: () => (/* binding */ __asyncValues),
  1404. /* harmony export */ __await: () => (/* binding */ __await),
  1405. /* harmony export */ __awaiter: () => (/* binding */ __awaiter),
  1406. /* harmony export */ __classPrivateFieldGet: () => (/* binding */ __classPrivateFieldGet),
  1407. /* harmony export */ __classPrivateFieldIn: () => (/* binding */ __classPrivateFieldIn),
  1408. /* harmony export */ __classPrivateFieldSet: () => (/* binding */ __classPrivateFieldSet),
  1409. /* harmony export */ __createBinding: () => (/* binding */ __createBinding),
  1410. /* harmony export */ __decorate: () => (/* binding */ __decorate),
  1411. /* harmony export */ __disposeResources: () => (/* binding */ __disposeResources),
  1412. /* harmony export */ __esDecorate: () => (/* binding */ __esDecorate),
  1413. /* harmony export */ __exportStar: () => (/* binding */ __exportStar),
  1414. /* harmony export */ __extends: () => (/* binding */ __extends),
  1415. /* harmony export */ __generator: () => (/* binding */ __generator),
  1416. /* harmony export */ __importDefault: () => (/* binding */ __importDefault),
  1417. /* harmony export */ __importStar: () => (/* binding */ __importStar),
  1418. /* harmony export */ __makeTemplateObject: () => (/* binding */ __makeTemplateObject),
  1419. /* harmony export */ __metadata: () => (/* binding */ __metadata),
  1420. /* harmony export */ __param: () => (/* binding */ __param),
  1421. /* harmony export */ __propKey: () => (/* binding */ __propKey),
  1422. /* harmony export */ __read: () => (/* binding */ __read),
  1423. /* harmony export */ __rest: () => (/* binding */ __rest),
  1424. /* harmony export */ __rewriteRelativeImportExtension: () => (/* binding */ __rewriteRelativeImportExtension),
  1425. /* harmony export */ __runInitializers: () => (/* binding */ __runInitializers),
  1426. /* harmony export */ __setFunctionName: () => (/* binding */ __setFunctionName),
  1427. /* harmony export */ __spread: () => (/* binding */ __spread),
  1428. /* harmony export */ __spreadArray: () => (/* binding */ __spreadArray),
  1429. /* harmony export */ __spreadArrays: () => (/* binding */ __spreadArrays),
  1430. /* harmony export */ __values: () => (/* binding */ __values),
  1431. /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
  1432. /* harmony export */ });
  1433. /******************************************************************************
  1434. Copyright (c) Microsoft Corporation.
  1435.  
  1436. Permission to use, copy, modify, and/or distribute this software for any
  1437. purpose with or without fee is hereby granted.
  1438.  
  1439. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
  1440. REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
  1441. AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
  1442. INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
  1443. LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
  1444. OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  1445. PERFORMANCE OF THIS SOFTWARE.
  1446. ***************************************************************************** */
  1447. /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
  1448.  
  1449. var extendStatics = function(d, b) {
  1450. extendStatics = Object.setPrototypeOf ||
  1451. ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
  1452. function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
  1453. return extendStatics(d, b);
  1454. };
  1455.  
  1456. function __extends(d, b) {
  1457. if (typeof b !== "function" && b !== null)
  1458. throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
  1459. extendStatics(d, b);
  1460. function __() { this.constructor = d; }
  1461. d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  1462. }
  1463.  
  1464. var __assign = function() {
  1465. __assign = Object.assign || function __assign(t) {
  1466. for (var s, i = 1, n = arguments.length; i < n; i++) {
  1467. s = arguments[i];
  1468. for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
  1469. }
  1470. return t;
  1471. }
  1472. return __assign.apply(this, arguments);
  1473. }
  1474.  
  1475. function __rest(s, e) {
  1476. var t = {};
  1477. for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
  1478. t[p] = s[p];
  1479. if (s != null && typeof Object.getOwnPropertySymbols === "function")
  1480. for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
  1481. if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
  1482. t[p[i]] = s[p[i]];
  1483. }
  1484. return t;
  1485. }
  1486.  
  1487. function __decorate(decorators, target, key, desc) {
  1488. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  1489. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  1490. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  1491. return c > 3 && r && Object.defineProperty(target, key, r), r;
  1492. }
  1493.  
  1494. function __param(paramIndex, decorator) {
  1495. return function (target, key) { decorator(target, key, paramIndex); }
  1496. }
  1497.  
  1498. function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
  1499. function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
  1500. var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
  1501. var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
  1502. var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
  1503. var _, done = false;
  1504. for (var i = decorators.length - 1; i >= 0; i--) {
  1505. var context = {};
  1506. for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
  1507. for (var p in contextIn.access) context.access[p] = contextIn.access[p];
  1508. context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
  1509. var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
  1510. if (kind === "accessor") {
  1511. if (result === void 0) continue;
  1512. if (result === null || typeof result !== "object") throw new TypeError("Object expected");
  1513. if (_ = accept(result.get)) descriptor.get = _;
  1514. if (_ = accept(result.set)) descriptor.set = _;
  1515. if (_ = accept(result.init)) initializers.unshift(_);
  1516. }
  1517. else if (_ = accept(result)) {
  1518. if (kind === "field") initializers.unshift(_);
  1519. else descriptor[key] = _;
  1520. }
  1521. }
  1522. if (target) Object.defineProperty(target, contextIn.name, descriptor);
  1523. done = true;
  1524. };
  1525.  
  1526. function __runInitializers(thisArg, initializers, value) {
  1527. var useValue = arguments.length > 2;
  1528. for (var i = 0; i < initializers.length; i++) {
  1529. value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
  1530. }
  1531. return useValue ? value : void 0;
  1532. };
  1533.  
  1534. function __propKey(x) {
  1535. return typeof x === "symbol" ? x : "".concat(x);
  1536. };
  1537.  
  1538. function __setFunctionName(f, name, prefix) {
  1539. if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
  1540. return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
  1541. };
  1542.  
  1543. function __metadata(metadataKey, metadataValue) {
  1544. if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
  1545. }
  1546.  
  1547. function __awaiter(thisArg, _arguments, P, generator) {
  1548. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  1549. return new (P || (P = Promise))(function (resolve, reject) {
  1550. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  1551. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  1552. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  1553. step((generator = generator.apply(thisArg, _arguments || [])).next());
  1554. });
  1555. }
  1556.  
  1557. function __generator(thisArg, body) {
  1558. var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
  1559. return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
  1560. function verb(n) { return function (v) { return step([n, v]); }; }
  1561. function step(op) {
  1562. if (f) throw new TypeError("Generator is already executing.");
  1563. while (g && (g = 0, op[0] && (_ = 0)), _) try {
  1564. if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
  1565. if (y = 0, t) op = [op[0] & 2, t.value];
  1566. switch (op[0]) {
  1567. case 0: case 1: t = op; break;
  1568. case 4: _.label++; return { value: op[1], done: false };
  1569. case 5: _.label++; y = op[1]; op = [0]; continue;
  1570. case 7: op = _.ops.pop(); _.trys.pop(); continue;
  1571. default:
  1572. if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
  1573. if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
  1574. if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
  1575. if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
  1576. if (t[2]) _.ops.pop();
  1577. _.trys.pop(); continue;
  1578. }
  1579. op = body.call(thisArg, _);
  1580. } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
  1581. if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
  1582. }
  1583. }
  1584.  
  1585. var __createBinding = Object.create ? (function(o, m, k, k2) {
  1586. if (k2 === undefined) k2 = k;
  1587. var desc = Object.getOwnPropertyDescriptor(m, k);
  1588. if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
  1589. desc = { enumerable: true, get: function() { return m[k]; } };
  1590. }
  1591. Object.defineProperty(o, k2, desc);
  1592. }) : (function(o, m, k, k2) {
  1593. if (k2 === undefined) k2 = k;
  1594. o[k2] = m[k];
  1595. });
  1596.  
  1597. function __exportStar(m, o) {
  1598. for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);
  1599. }
  1600.  
  1601. function __values(o) {
  1602. var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
  1603. if (m) return m.call(o);
  1604. if (o && typeof o.length === "number") return {
  1605. next: function () {
  1606. if (o && i >= o.length) o = void 0;
  1607. return { value: o && o[i++], done: !o };
  1608. }
  1609. };
  1610. throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
  1611. }
  1612.  
  1613. function __read(o, n) {
  1614. var m = typeof Symbol === "function" && o[Symbol.iterator];
  1615. if (!m) return o;
  1616. var i = m.call(o), r, ar = [], e;
  1617. try {
  1618. while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
  1619. }
  1620. catch (error) { e = { error: error }; }
  1621. finally {
  1622. try {
  1623. if (r && !r.done && (m = i["return"])) m.call(i);
  1624. }
  1625. finally { if (e) throw e.error; }
  1626. }
  1627. return ar;
  1628. }
  1629.  
  1630. /** @deprecated */
  1631. function __spread() {
  1632. for (var ar = [], i = 0; i < arguments.length; i++)
  1633. ar = ar.concat(__read(arguments[i]));
  1634. return ar;
  1635. }
  1636.  
  1637. /** @deprecated */
  1638. function __spreadArrays() {
  1639. for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
  1640. for (var r = Array(s), k = 0, i = 0; i < il; i++)
  1641. for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
  1642. r[k] = a[j];
  1643. return r;
  1644. }
  1645.  
  1646. function __spreadArray(to, from, pack) {
  1647. if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
  1648. if (ar || !(i in from)) {
  1649. if (!ar) ar = Array.prototype.slice.call(from, 0, i);
  1650. ar[i] = from[i];
  1651. }
  1652. }
  1653. return to.concat(ar || Array.prototype.slice.call(from));
  1654. }
  1655.  
  1656. function __await(v) {
  1657. return this instanceof __await ? (this.v = v, this) : new __await(v);
  1658. }
  1659.  
  1660. function __asyncGenerator(thisArg, _arguments, generator) {
  1661. if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
  1662. var g = generator.apply(thisArg, _arguments || []), i, q = [];
  1663. return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
  1664. function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
  1665. function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
  1666. function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
  1667. function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
  1668. function fulfill(value) { resume("next", value); }
  1669. function reject(value) { resume("throw", value); }
  1670. function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
  1671. }
  1672.  
  1673. function __asyncDelegator(o) {
  1674. var i, p;
  1675. return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i;
  1676. function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }
  1677. }
  1678.  
  1679. function __asyncValues(o) {
  1680. if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
  1681. var m = o[Symbol.asyncIterator], i;
  1682. return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
  1683. function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
  1684. function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
  1685. }
  1686.  
  1687. function __makeTemplateObject(cooked, raw) {
  1688. if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
  1689. return cooked;
  1690. };
  1691.  
  1692. var __setModuleDefault = Object.create ? (function(o, v) {
  1693. Object.defineProperty(o, "default", { enumerable: true, value: v });
  1694. }) : function(o, v) {
  1695. o["default"] = v;
  1696. };
  1697.  
  1698. var ownKeys = function(o) {
  1699. ownKeys = Object.getOwnPropertyNames || function (o) {
  1700. var ar = [];
  1701. for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
  1702. return ar;
  1703. };
  1704. return ownKeys(o);
  1705. };
  1706.  
  1707. function __importStar(mod) {
  1708. if (mod && mod.__esModule) return mod;
  1709. var result = {};
  1710. if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
  1711. __setModuleDefault(result, mod);
  1712. return result;
  1713. }
  1714.  
  1715. function __importDefault(mod) {
  1716. return (mod && mod.__esModule) ? mod : { default: mod };
  1717. }
  1718.  
  1719. function __classPrivateFieldGet(receiver, state, kind, f) {
  1720. if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
  1721. if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
  1722. return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
  1723. }
  1724.  
  1725. function __classPrivateFieldSet(receiver, state, value, kind, f) {
  1726. if (kind === "m") throw new TypeError("Private method is not writable");
  1727. if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
  1728. if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
  1729. return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
  1730. }
  1731.  
  1732. function __classPrivateFieldIn(state, receiver) {
  1733. if (receiver === null || (typeof receiver !== "object" && typeof receiver !== "function")) throw new TypeError("Cannot use 'in' operator on non-object");
  1734. return typeof state === "function" ? receiver === state : state.has(receiver);
  1735. }
  1736.  
  1737. function __addDisposableResource(env, value, async) {
  1738. if (value !== null && value !== void 0) {
  1739. if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
  1740. var dispose, inner;
  1741. if (async) {
  1742. if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
  1743. dispose = value[Symbol.asyncDispose];
  1744. }
  1745. if (dispose === void 0) {
  1746. if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
  1747. dispose = value[Symbol.dispose];
  1748. if (async) inner = dispose;
  1749. }
  1750. if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
  1751. if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
  1752. env.stack.push({ value: value, dispose: dispose, async: async });
  1753. }
  1754. else if (async) {
  1755. env.stack.push({ async: true });
  1756. }
  1757. return value;
  1758. }
  1759.  
  1760. var _SuppressedError = typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
  1761. var e = new Error(message);
  1762. return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
  1763. };
  1764.  
  1765. function __disposeResources(env) {
  1766. function fail(e) {
  1767. env.error = env.hasError ? new _SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
  1768. env.hasError = true;
  1769. }
  1770. var r, s = 0;
  1771. function next() {
  1772. while (r = env.stack.pop()) {
  1773. try {
  1774. if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
  1775. if (r.dispose) {
  1776. var result = r.dispose.call(r.value);
  1777. if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
  1778. }
  1779. else s |= 1;
  1780. }
  1781. catch (e) {
  1782. fail(e);
  1783. }
  1784. }
  1785. if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
  1786. if (env.hasError) throw env.error;
  1787. }
  1788. return next();
  1789. }
  1790.  
  1791. function __rewriteRelativeImportExtension(path, preserveJsx) {
  1792. if (typeof path === "string" && /^\.\.?\//.test(path)) {
  1793. return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
  1794. return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
  1795. });
  1796. }
  1797. return path;
  1798. }
  1799.  
  1800. /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({
  1801. __extends,
  1802. __assign,
  1803. __rest,
  1804. __decorate,
  1805. __param,
  1806. __esDecorate,
  1807. __runInitializers,
  1808. __propKey,
  1809. __setFunctionName,
  1810. __metadata,
  1811. __awaiter,
  1812. __generator,
  1813. __createBinding,
  1814. __exportStar,
  1815. __values,
  1816. __read,
  1817. __spread,
  1818. __spreadArrays,
  1819. __spreadArray,
  1820. __await,
  1821. __asyncGenerator,
  1822. __asyncDelegator,
  1823. __asyncValues,
  1824. __makeTemplateObject,
  1825. __importStar,
  1826. __importDefault,
  1827. __classPrivateFieldGet,
  1828. __classPrivateFieldSet,
  1829. __classPrivateFieldIn,
  1830. __addDisposableResource,
  1831. __disposeResources,
  1832. __rewriteRelativeImportExtension,
  1833. });
  1834.  
  1835.  
  1836. /***/ }),
  1837.  
  1838. /***/ 641:
  1839. /***/ ((__unused_webpack_module, exports) => {
  1840.  
  1841. "use strict";
  1842.  
  1843. Object.defineProperty(exports, "__esModule", ({ value: true }));
  1844. exports.createTimestampLinks = createTimestampLinks;
  1845. async function createTimestampLinks(element) {
  1846. var _a;
  1847. const libraryHref = (_a = document.querySelector('.secondary-nav-item--library a')) === null || _a === void 0 ? void 0 : _a.href;
  1848. if (!libraryHref) {
  1849. return;
  1850. }
  1851. const cells = element.querySelectorAll('.chartlist-timestamp');
  1852. for (const cell of cells) {
  1853. const span = cell.querySelector('span[title]');
  1854. if (span === null || span.parentNode !== cell) {
  1855. continue;
  1856. }
  1857. let date;
  1858. if (cell.classList.contains('chartlist-timestamp--lang-en')) {
  1859. date = new Date(Date.parse(span.title.split(',')[0]));
  1860. }
  1861. else {
  1862. // Languages other than English are not supported.
  1863. continue;
  1864. }
  1865. const dateString = getDateString(date);
  1866. const link = document.createElement('a');
  1867. link.href = `${libraryHref}?from=${dateString}&to=${dateString}`;
  1868. cell.insertBefore(link, span);
  1869. link.appendChild(span);
  1870. }
  1871. }
  1872. function getDateString(date) {
  1873. let s = date.getFullYear() + '-';
  1874. const month = date.getMonth() + 1;
  1875. if (month < 10)
  1876. s += '0';
  1877. s += month + '-';
  1878. const day = date.getDate();
  1879. if (day < 10)
  1880. s += '0';
  1881. s += day;
  1882. return s;
  1883. }
  1884.  
  1885.  
  1886. /***/ }),
  1887.  
  1888. /***/ 646:
  1889. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  1890.  
  1891. "use strict";
  1892.  
  1893. Object.defineProperty(exports, "__esModule", ({ value: true }));
  1894. exports.withTimeout = void 0;
  1895. var tslib_1 = __webpack_require__(635);
  1896. /* eslint-disable @typescript-eslint/no-explicit-any */
  1897. var errors_1 = __webpack_require__(586);
  1898. function withTimeout(sync, timeout, timeoutError) {
  1899. var _this = this;
  1900. if (timeoutError === void 0) { timeoutError = errors_1.E_TIMEOUT; }
  1901. return {
  1902. acquire: function (weightOrPriority, priority) {
  1903. var weight;
  1904. if (isSemaphore(sync)) {
  1905. weight = weightOrPriority;
  1906. }
  1907. else {
  1908. weight = undefined;
  1909. priority = weightOrPriority;
  1910. }
  1911. if (weight !== undefined && weight <= 0) {
  1912. throw new Error("invalid weight ".concat(weight, ": must be positive"));
  1913. }
  1914. return new Promise(function (resolve, reject) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
  1915. var isTimeout, handle, ticket, release, e_1;
  1916. return tslib_1.__generator(this, function (_a) {
  1917. switch (_a.label) {
  1918. case 0:
  1919. isTimeout = false;
  1920. handle = setTimeout(function () {
  1921. isTimeout = true;
  1922. reject(timeoutError);
  1923. }, timeout);
  1924. _a.label = 1;
  1925. case 1:
  1926. _a.trys.push([1, 3, , 4]);
  1927. return [4 /*yield*/, (isSemaphore(sync)
  1928. ? sync.acquire(weight, priority)
  1929. : sync.acquire(priority))];
  1930. case 2:
  1931. ticket = _a.sent();
  1932. if (isTimeout) {
  1933. release = Array.isArray(ticket) ? ticket[1] : ticket;
  1934. release();
  1935. }
  1936. else {
  1937. clearTimeout(handle);
  1938. resolve(ticket);
  1939. }
  1940. return [3 /*break*/, 4];
  1941. case 3:
  1942. e_1 = _a.sent();
  1943. if (!isTimeout) {
  1944. clearTimeout(handle);
  1945. reject(e_1);
  1946. }
  1947. return [3 /*break*/, 4];
  1948. case 4: return [2 /*return*/];
  1949. }
  1950. });
  1951. }); });
  1952. },
  1953. runExclusive: function (callback, weight, priority) {
  1954. return tslib_1.__awaiter(this, void 0, void 0, function () {
  1955. var release, ticket;
  1956. return tslib_1.__generator(this, function (_a) {
  1957. switch (_a.label) {
  1958. case 0:
  1959. release = function () { return undefined; };
  1960. _a.label = 1;
  1961. case 1:
  1962. _a.trys.push([1, , 7, 8]);
  1963. return [4 /*yield*/, this.acquire(weight, priority)];
  1964. case 2:
  1965. ticket = _a.sent();
  1966. if (!Array.isArray(ticket)) return [3 /*break*/, 4];
  1967. release = ticket[1];
  1968. return [4 /*yield*/, callback(ticket[0])];
  1969. case 3: return [2 /*return*/, _a.sent()];
  1970. case 4:
  1971. release = ticket;
  1972. return [4 /*yield*/, callback()];
  1973. case 5: return [2 /*return*/, _a.sent()];
  1974. case 6: return [3 /*break*/, 8];
  1975. case 7:
  1976. release();
  1977. return [7 /*endfinally*/];
  1978. case 8: return [2 /*return*/];
  1979. }
  1980. });
  1981. });
  1982. },
  1983. release: function (weight) {
  1984. sync.release(weight);
  1985. },
  1986. cancel: function () {
  1987. return sync.cancel();
  1988. },
  1989. waitForUnlock: function (weightOrPriority, priority) {
  1990. var weight;
  1991. if (isSemaphore(sync)) {
  1992. weight = weightOrPriority;
  1993. }
  1994. else {
  1995. weight = undefined;
  1996. priority = weightOrPriority;
  1997. }
  1998. if (weight !== undefined && weight <= 0) {
  1999. throw new Error("invalid weight ".concat(weight, ": must be positive"));
  2000. }
  2001. return new Promise(function (resolve, reject) {
  2002. var handle = setTimeout(function () { return reject(timeoutError); }, timeout);
  2003. (isSemaphore(sync)
  2004. ? sync.waitForUnlock(weight, priority)
  2005. : sync.waitForUnlock(priority)).then(function () {
  2006. clearTimeout(handle);
  2007. resolve();
  2008. });
  2009. });
  2010. },
  2011. isLocked: function () { return sync.isLocked(); },
  2012. getValue: function () { return sync.getValue(); },
  2013. setValue: function (value) { return sync.setValue(value); },
  2014. };
  2015. }
  2016. exports.withTimeout = withTimeout;
  2017. function isSemaphore(sync) {
  2018. return sync.getValue !== undefined;
  2019. }
  2020.  
  2021.  
  2022. /***/ }),
  2023.  
  2024. /***/ 692:
  2025. /***/ ((module) => {
  2026.  
  2027. async function* asyncPool(concurrency, iterable, iteratorFn) {
  2028. const executing = new Set();
  2029. async function consume() {
  2030. const [promise, value] = await Promise.race(executing);
  2031. executing.delete(promise);
  2032. return value;
  2033. }
  2034. for (const item of iterable) {
  2035. // Wrap iteratorFn() in an async fn to ensure we get a promise.
  2036. // Then expose such promise, so it's possible to later reference and
  2037. // remove it from the executing pool.
  2038. const promise = (async () => await iteratorFn(item, iterable))().then(
  2039. value => [promise, value]
  2040. );
  2041. executing.add(promise);
  2042. if (executing.size >= concurrency) {
  2043. yield await consume();
  2044. }
  2045. }
  2046. while (executing.size) {
  2047. yield await consume();
  2048. }
  2049. }
  2050.  
  2051. module.exports = asyncPool;
  2052.  
  2053.  
  2054. /***/ }),
  2055.  
  2056. /***/ 693:
  2057. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  2058.  
  2059. "use strict";
  2060.  
  2061. Object.defineProperty(exports, "__esModule", ({ value: true }));
  2062. exports.tryAcquire = exports.withTimeout = exports.Semaphore = exports.Mutex = void 0;
  2063. var tslib_1 = __webpack_require__(635);
  2064. var Mutex_1 = __webpack_require__(406);
  2065. Object.defineProperty(exports, "Mutex", ({ enumerable: true, get: function () { return Mutex_1.default; } }));
  2066. var Semaphore_1 = __webpack_require__(919);
  2067. Object.defineProperty(exports, "Semaphore", ({ enumerable: true, get: function () { return Semaphore_1.default; } }));
  2068. var withTimeout_1 = __webpack_require__(646);
  2069. Object.defineProperty(exports, "withTimeout", ({ enumerable: true, get: function () { return withTimeout_1.withTimeout; } }));
  2070. var tryAcquire_1 = __webpack_require__(746);
  2071. Object.defineProperty(exports, "tryAcquire", ({ enumerable: true, get: function () { return tryAcquire_1.tryAcquire; } }));
  2072. tslib_1.__exportStar(__webpack_require__(586), exports);
  2073.  
  2074.  
  2075. /***/ }),
  2076.  
  2077. /***/ 694:
  2078. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  2079.  
  2080. "use strict";
  2081.  
  2082. Object.defineProperty(exports, "__esModule", ({ value: true }));
  2083. exports.LoadingModal = void 0;
  2084. const constants_1 = __webpack_require__(921);
  2085. const Modal_1 = __webpack_require__(946);
  2086. class LoadingModal extends Modal_1.Modal {
  2087. constructor(title, options) {
  2088. const body = `
  2089. <div class="${constants_1.namespace}-loading">
  2090. <div class="${constants_1.namespace}-progress"></div>
  2091. </div>`;
  2092. super(title, body, options);
  2093. this.completed = false;
  2094. this.steps = [];
  2095. this.weight = 0;
  2096. this.progress = this.element.querySelector(`.${constants_1.namespace}-progress`);
  2097. }
  2098. refreshProgress() {
  2099. switch (this.options && this.options.display) {
  2100. case 'count':
  2101. this.progress.textContent = `${this.steps.filter((s) => s.completed).length} / ${this.steps.length}`;
  2102. break;
  2103. case 'percentage':
  2104. this.progress.textContent = Math.floor(getCompletionRatio(this.steps) * 100) + '%';
  2105. break;
  2106. }
  2107. }
  2108. }
  2109. exports.LoadingModal = LoadingModal;
  2110. // calculates the completion ratio from a tree of steps with weights and child steps
  2111. function getCompletionRatio(steps) {
  2112. const totalWeight = steps.map((s) => s.weight).reduce((a, b) => a + b, 0);
  2113. if (totalWeight === 0)
  2114. return 0;
  2115. const completedWeight = steps.map((s) => s.weight * (s.completed ? 1 : getCompletionRatio(s.steps))).reduce((a, b) => a + b, 0);
  2116. return completedWeight / totalWeight;
  2117. }
  2118.  
  2119.  
  2120. /***/ }),
  2121.  
  2122. /***/ 746:
  2123. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  2124.  
  2125. "use strict";
  2126.  
  2127. Object.defineProperty(exports, "__esModule", ({ value: true }));
  2128. exports.tryAcquire = void 0;
  2129. var errors_1 = __webpack_require__(586);
  2130. var withTimeout_1 = __webpack_require__(646);
  2131. // eslint-disable-next-lisne @typescript-eslint/explicit-module-boundary-types
  2132. function tryAcquire(sync, alreadyAcquiredError) {
  2133. if (alreadyAcquiredError === void 0) { alreadyAcquiredError = errors_1.E_ALREADY_LOCKED; }
  2134. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  2135. return (0, withTimeout_1.withTimeout)(sync, 0, alreadyAcquiredError);
  2136. }
  2137. exports.tryAcquire = tryAcquire;
  2138.  
  2139.  
  2140. /***/ }),
  2141.  
  2142. /***/ 919:
  2143. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  2144.  
  2145. "use strict";
  2146.  
  2147. Object.defineProperty(exports, "__esModule", ({ value: true }));
  2148. var tslib_1 = __webpack_require__(635);
  2149. var errors_1 = __webpack_require__(586);
  2150. var Semaphore = /** @class */ (function () {
  2151. function Semaphore(_value, _cancelError) {
  2152. if (_cancelError === void 0) { _cancelError = errors_1.E_CANCELED; }
  2153. this._value = _value;
  2154. this._cancelError = _cancelError;
  2155. this._queue = [];
  2156. this._weightedWaiters = [];
  2157. }
  2158. Semaphore.prototype.acquire = function (weight, priority) {
  2159. var _this = this;
  2160. if (weight === void 0) { weight = 1; }
  2161. if (priority === void 0) { priority = 0; }
  2162. if (weight <= 0)
  2163. throw new Error("invalid weight ".concat(weight, ": must be positive"));
  2164. return new Promise(function (resolve, reject) {
  2165. var task = { resolve: resolve, reject: reject, weight: weight, priority: priority };
  2166. var i = findIndexFromEnd(_this._queue, function (other) { return priority <= other.priority; });
  2167. if (i === -1 && weight <= _this._value) {
  2168. // Needs immediate dispatch, skip the queue
  2169. _this._dispatchItem(task);
  2170. }
  2171. else {
  2172. _this._queue.splice(i + 1, 0, task);
  2173. }
  2174. });
  2175. };
  2176. Semaphore.prototype.runExclusive = function (callback_1) {
  2177. return tslib_1.__awaiter(this, arguments, void 0, function (callback, weight, priority) {
  2178. var _a, value, release;
  2179. if (weight === void 0) { weight = 1; }
  2180. if (priority === void 0) { priority = 0; }
  2181. return tslib_1.__generator(this, function (_b) {
  2182. switch (_b.label) {
  2183. case 0: return [4 /*yield*/, this.acquire(weight, priority)];
  2184. case 1:
  2185. _a = _b.sent(), value = _a[0], release = _a[1];
  2186. _b.label = 2;
  2187. case 2:
  2188. _b.trys.push([2, , 4, 5]);
  2189. return [4 /*yield*/, callback(value)];
  2190. case 3: return [2 /*return*/, _b.sent()];
  2191. case 4:
  2192. release();
  2193. return [7 /*endfinally*/];
  2194. case 5: return [2 /*return*/];
  2195. }
  2196. });
  2197. });
  2198. };
  2199. Semaphore.prototype.waitForUnlock = function (weight, priority) {
  2200. var _this = this;
  2201. if (weight === void 0) { weight = 1; }
  2202. if (priority === void 0) { priority = 0; }
  2203. if (weight <= 0)
  2204. throw new Error("invalid weight ".concat(weight, ": must be positive"));
  2205. if (this._couldLockImmediately(weight, priority)) {
  2206. return Promise.resolve();
  2207. }
  2208. else {
  2209. return new Promise(function (resolve) {
  2210. if (!_this._weightedWaiters[weight - 1])
  2211. _this._weightedWaiters[weight - 1] = [];
  2212. insertSorted(_this._weightedWaiters[weight - 1], { resolve: resolve, priority: priority });
  2213. });
  2214. }
  2215. };
  2216. Semaphore.prototype.isLocked = function () {
  2217. return this._value <= 0;
  2218. };
  2219. Semaphore.prototype.getValue = function () {
  2220. return this._value;
  2221. };
  2222. Semaphore.prototype.setValue = function (value) {
  2223. this._value = value;
  2224. this._dispatchQueue();
  2225. };
  2226. Semaphore.prototype.release = function (weight) {
  2227. if (weight === void 0) { weight = 1; }
  2228. if (weight <= 0)
  2229. throw new Error("invalid weight ".concat(weight, ": must be positive"));
  2230. this._value += weight;
  2231. this._dispatchQueue();
  2232. };
  2233. Semaphore.prototype.cancel = function () {
  2234. var _this = this;
  2235. this._queue.forEach(function (entry) { return entry.reject(_this._cancelError); });
  2236. this._queue = [];
  2237. };
  2238. Semaphore.prototype._dispatchQueue = function () {
  2239. this._drainUnlockWaiters();
  2240. while (this._queue.length > 0 && this._queue[0].weight <= this._value) {
  2241. this._dispatchItem(this._queue.shift());
  2242. this._drainUnlockWaiters();
  2243. }
  2244. };
  2245. Semaphore.prototype._dispatchItem = function (item) {
  2246. var previousValue = this._value;
  2247. this._value -= item.weight;
  2248. item.resolve([previousValue, this._newReleaser(item.weight)]);
  2249. };
  2250. Semaphore.prototype._newReleaser = function (weight) {
  2251. var _this = this;
  2252. var called = false;
  2253. return function () {
  2254. if (called)
  2255. return;
  2256. called = true;
  2257. _this.release(weight);
  2258. };
  2259. };
  2260. Semaphore.prototype._drainUnlockWaiters = function () {
  2261. if (this._queue.length === 0) {
  2262. for (var weight = this._value; weight > 0; weight--) {
  2263. var waiters = this._weightedWaiters[weight - 1];
  2264. if (!waiters)
  2265. continue;
  2266. waiters.forEach(function (waiter) { return waiter.resolve(); });
  2267. this._weightedWaiters[weight - 1] = [];
  2268. }
  2269. }
  2270. else {
  2271. var queuedPriority_1 = this._queue[0].priority;
  2272. for (var weight = this._value; weight > 0; weight--) {
  2273. var waiters = this._weightedWaiters[weight - 1];
  2274. if (!waiters)
  2275. continue;
  2276. var i = waiters.findIndex(function (waiter) { return waiter.priority <= queuedPriority_1; });
  2277. (i === -1 ? waiters : waiters.splice(0, i))
  2278. .forEach((function (waiter) { return waiter.resolve(); }));
  2279. }
  2280. }
  2281. };
  2282. Semaphore.prototype._couldLockImmediately = function (weight, priority) {
  2283. return (this._queue.length === 0 || this._queue[0].priority < priority) &&
  2284. weight <= this._value;
  2285. };
  2286. return Semaphore;
  2287. }());
  2288. function insertSorted(a, v) {
  2289. var i = findIndexFromEnd(a, function (other) { return v.priority <= other.priority; });
  2290. a.splice(i + 1, 0, v);
  2291. }
  2292. function findIndexFromEnd(a, predicate) {
  2293. for (var i = a.length - 1; i >= 0; i--) {
  2294. if (predicate(a[i])) {
  2295. return i;
  2296. }
  2297. }
  2298. return -1;
  2299. }
  2300. exports["default"] = Semaphore;
  2301.  
  2302.  
  2303. /***/ }),
  2304.  
  2305. /***/ 921:
  2306. /***/ ((__unused_webpack_module, exports) => {
  2307.  
  2308. "use strict";
  2309.  
  2310. Object.defineProperty(exports, "__esModule", ({ value: true }));
  2311. exports.namespace = void 0;
  2312. exports.namespace = 'lastfm-bulk-edit';
  2313.  
  2314.  
  2315. /***/ }),
  2316.  
  2317. /***/ 946:
  2318. /***/ ((__unused_webpack_module, exports) => {
  2319.  
  2320. "use strict";
  2321.  
  2322. Object.defineProperty(exports, "__esModule", ({ value: true }));
  2323. exports.Modal = void 0;
  2324. class Modal {
  2325. constructor(title, body, options) {
  2326. this.addedClass = false;
  2327. this.element = document.createElement('div');
  2328. this.options = options;
  2329. const fragment = modalTemplate.content.cloneNode(true);
  2330. const modalTitle = fragment.querySelector('.modal-title');
  2331. if (title instanceof Element) {
  2332. modalTitle.insertAdjacentElement('beforeend', title);
  2333. }
  2334. else {
  2335. modalTitle.insertAdjacentHTML('beforeend', title);
  2336. }
  2337. const modalBody = fragment.querySelector('.modal-body');
  2338. if (body instanceof Element) {
  2339. modalBody.insertAdjacentElement('beforeend', body);
  2340. }
  2341. else {
  2342. modalBody.insertAdjacentHTML('beforeend', body);
  2343. }
  2344. if (options && options.dismissible) {
  2345. // create X button that closes the modal
  2346. const closeButton = document.createElement('button');
  2347. closeButton.className = 'modal-dismiss sr-only';
  2348. closeButton.textContent = 'Close';
  2349. closeButton.addEventListener('click', () => this.hide());
  2350. // create modal actions div
  2351. const modalActions = document.createElement('div');
  2352. modalActions.className = 'modal-actions';
  2353. modalActions.appendChild(closeButton);
  2354. // append modal actions to modal content
  2355. const modalContent = fragment.querySelector('.modal-content');
  2356. modalContent.insertBefore(modalActions, modalContent.firstElementChild);
  2357. // close modal when user clicks outside modal
  2358. const popupWrapper = fragment.querySelector('.popup_wrapper');
  2359. popupWrapper.addEventListener('click', (event) => {
  2360. if (event.target instanceof Node && !modalContent.contains(event.target)) {
  2361. this.hide();
  2362. }
  2363. });
  2364. }
  2365. this.element.appendChild(fragment);
  2366. }
  2367. get isAttached() {
  2368. return !!this.element.parentNode;
  2369. }
  2370. show() {
  2371. if (this.element.parentNode)
  2372. return;
  2373. document.body.appendChild(this.element);
  2374. if (!document.documentElement.classList.contains('popup_visible')) {
  2375. document.documentElement.classList.add('popup_visible');
  2376. this.addedClass = true;
  2377. }
  2378. }
  2379. hide() {
  2380. if (!this.element.parentNode)
  2381. return;
  2382. this.element.parentNode.removeChild(this.element);
  2383. if (this.addedClass) {
  2384. document.documentElement.classList.remove('popup_visible');
  2385. this.addedClass = false;
  2386. }
  2387. if (this.options && this.options.events && this.options.events.hide) {
  2388. this.options.events.hide();
  2389. }
  2390. }
  2391. }
  2392. exports.Modal = Modal;
  2393. const modalTemplate = document.createElement('template');
  2394. modalTemplate.innerHTML = `
  2395. <div class="popup_background"
  2396. style="opacity: 0.8; visibility: visible; background-color: rgb(0, 0, 0); position: fixed; top: 0px; right: 0px; bottom: 0px; left: 0px;">
  2397. </div>
  2398. <div class="popup_wrapper popup_wrapper_visible" style="opacity: 1; visibility: visible; position: fixed; overflow: auto; width: 100%; height: 100%; top: 0px; left: 0px; text-align: center;">
  2399. <div class="modal-dialog popup_content" role="dialog" aria-labelledby="modal-label" data-popup-initialized="true" aria-hidden="false" style="opacity: 1; visibility: visible; pointer-events: auto; display: inline-block; outline: none; text-align: left; position: relative; vertical-align: middle;" tabindex="-1">
  2400. <div class="modal-content">
  2401. <div class="modal-body">
  2402. <h2 class="modal-title"></h2>
  2403. </div>
  2404. </div>
  2405. </div>
  2406. <div class="popup_align" style="display: inline-block; vertical-align: middle; height: 100%;"></div>
  2407. </div>`;
  2408.  
  2409.  
  2410. /***/ })
  2411.  
  2412. /******/ });
  2413. /************************************************************************/
  2414. /******/ // The module cache
  2415. /******/ var __webpack_module_cache__ = {};
  2416. /******/
  2417. /******/ // The require function
  2418. /******/ function __webpack_require__(moduleId) {
  2419. /******/ // Check if module is in cache
  2420. /******/ var cachedModule = __webpack_module_cache__[moduleId];
  2421. /******/ if (cachedModule !== undefined) {
  2422. /******/ return cachedModule.exports;
  2423. /******/ }
  2424. /******/ // Create a new module (and put it into the cache)
  2425. /******/ var module = __webpack_module_cache__[moduleId] = {
  2426. /******/ // no module.id needed
  2427. /******/ // no module.loaded needed
  2428. /******/ exports: {}
  2429. /******/ };
  2430. /******/
  2431. /******/ // Execute the module function
  2432. /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  2433. /******/
  2434. /******/ // Return the exports of the module
  2435. /******/ return module.exports;
  2436. /******/ }
  2437. /******/
  2438. /************************************************************************/
  2439. /******/ /* webpack/runtime/define property getters */
  2440. /******/ (() => {
  2441. /******/ // define getter functions for harmony exports
  2442. /******/ __webpack_require__.d = (exports, definition) => {
  2443. /******/ for(var key in definition) {
  2444. /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
  2445. /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
  2446. /******/ }
  2447. /******/ }
  2448. /******/ };
  2449. /******/ })();
  2450. /******/
  2451. /******/ /* webpack/runtime/hasOwnProperty shorthand */
  2452. /******/ (() => {
  2453. /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
  2454. /******/ })();
  2455. /******/
  2456. /******/ /* webpack/runtime/make namespace object */
  2457. /******/ (() => {
  2458. /******/ // define __esModule on exports
  2459. /******/ __webpack_require__.r = (exports) => {
  2460. /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
  2461. /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  2462. /******/ }
  2463. /******/ Object.defineProperty(exports, '__esModule', { value: true });
  2464. /******/ };
  2465. /******/ })();
  2466. /******/
  2467. /************************************************************************/
  2468. /******/
  2469. /******/ // startup
  2470. /******/ // Load entry module and return exports
  2471. /******/ // This entry module is referenced by other modules so it can't be inlined
  2472. /******/ var __webpack_exports__ = __webpack_require__(156);
  2473. /******/
  2474. /******/ })()
  2475. ;