// ==UserScript==
// @name Last.fm Bulk Edit
// @description Bulk edit your scrobbles for any artist or album on Last.fm at once.
// @version 1.6.1
// @author Rudey
// @homepage https://github.com/RudeySH/lastfm-bulk-edit
// @supportURL https://github.com/RudeySH/lastfm-bulk-edit/issues
// @match https://www.last.fm/*
// @downloadURL https://raw.githubusercontent.com/RudeySH/lastfm-bulk-edit/main/dist/lastfm-bulk-edit.user.js
// @icon https://raw.githubusercontent.com/RudeySH/lastfm-bulk-edit/main/img/icon.png
// @license AGPL-3.0-or-later
// @namespace https://github.com/RudeySH/lastfm-bulk-edit
// @require https://cdnjs.cloudflare.com/ajax/libs/he/1.2.0/he.min.js
// @updateURL https://raw.githubusercontent.com/RudeySH/lastfm-bulk-edit/main/dist/lastfm-bulk-edit.meta.js
// ==/UserScript==
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ 406:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
var tslib_1 = __webpack_require__(653);
var Semaphore_1 = __webpack_require__(919);
var Mutex = /** @class */ (function () {
function Mutex(cancelError) {
this._semaphore = new Semaphore_1.default(1, cancelError);
}
Mutex.prototype.acquire = function () {
return tslib_1.__awaiter(this, arguments, void 0, function (priority) {
var _a, releaser;
if (priority === void 0) { priority = 0; }
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, this._semaphore.acquire(1, priority)];
case 1:
_a = _b.sent(), releaser = _a[1];
return [2 /*return*/, releaser];
}
});
});
};
Mutex.prototype.runExclusive = function (callback, priority) {
if (priority === void 0) { priority = 0; }
return this._semaphore.runExclusive(function () { return callback(); }, 1, priority);
};
Mutex.prototype.isLocked = function () {
return this._semaphore.isLocked();
};
Mutex.prototype.waitForUnlock = function (priority) {
if (priority === void 0) { priority = 0; }
return this._semaphore.waitForUnlock(1, priority);
};
Mutex.prototype.release = function () {
if (this._semaphore.isLocked())
this._semaphore.release();
};
Mutex.prototype.cancel = function () {
return this._semaphore.cancel();
};
return Mutex;
}());
exports["default"] = Mutex;
/***/ }),
/***/ 919:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
var tslib_1 = __webpack_require__(653);
var errors_1 = __webpack_require__(586);
var Semaphore = /** @class */ (function () {
function Semaphore(_value, _cancelError) {
if (_cancelError === void 0) { _cancelError = errors_1.E_CANCELED; }
this._value = _value;
this._cancelError = _cancelError;
this._queue = [];
this._weightedWaiters = [];
}
Semaphore.prototype.acquire = function (weight, priority) {
var _this = this;
if (weight === void 0) { weight = 1; }
if (priority === void 0) { priority = 0; }
if (weight <= 0)
throw new Error("invalid weight ".concat(weight, ": must be positive"));
return new Promise(function (resolve, reject) {
var task = { resolve: resolve, reject: reject, weight: weight, priority: priority };
var i = findIndexFromEnd(_this._queue, function (other) { return priority <= other.priority; });
if (i === -1 && weight <= _this._value) {
// Needs immediate dispatch, skip the queue
_this._dispatchItem(task);
}
else {
_this._queue.splice(i + 1, 0, task);
}
});
};
Semaphore.prototype.runExclusive = function (callback_1) {
return tslib_1.__awaiter(this, arguments, void 0, function (callback, weight, priority) {
var _a, value, release;
if (weight === void 0) { weight = 1; }
if (priority === void 0) { priority = 0; }
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, this.acquire(weight, priority)];
case 1:
_a = _b.sent(), value = _a[0], release = _a[1];
_b.label = 2;
case 2:
_b.trys.push([2, , 4, 5]);
return [4 /*yield*/, callback(value)];
case 3: return [2 /*return*/, _b.sent()];
case 4:
release();
return [7 /*endfinally*/];
case 5: return [2 /*return*/];
}
});
});
};
Semaphore.prototype.waitForUnlock = function (weight, priority) {
var _this = this;
if (weight === void 0) { weight = 1; }
if (priority === void 0) { priority = 0; }
if (weight <= 0)
throw new Error("invalid weight ".concat(weight, ": must be positive"));
if (this._couldLockImmediately(weight, priority)) {
return Promise.resolve();
}
else {
return new Promise(function (resolve) {
if (!_this._weightedWaiters[weight - 1])
_this._weightedWaiters[weight - 1] = [];
insertSorted(_this._weightedWaiters[weight - 1], { resolve: resolve, priority: priority });
});
}
};
Semaphore.prototype.isLocked = function () {
return this._value <= 0;
};
Semaphore.prototype.getValue = function () {
return this._value;
};
Semaphore.prototype.setValue = function (value) {
this._value = value;
this._dispatchQueue();
};
Semaphore.prototype.release = function (weight) {
if (weight === void 0) { weight = 1; }
if (weight <= 0)
throw new Error("invalid weight ".concat(weight, ": must be positive"));
this._value += weight;
this._dispatchQueue();
};
Semaphore.prototype.cancel = function () {
var _this = this;
this._queue.forEach(function (entry) { return entry.reject(_this._cancelError); });
this._queue = [];
};
Semaphore.prototype._dispatchQueue = function () {
this._drainUnlockWaiters();
while (this._queue.length > 0 && this._queue[0].weight <= this._value) {
this._dispatchItem(this._queue.shift());
this._drainUnlockWaiters();
}
};
Semaphore.prototype._dispatchItem = function (item) {
var previousValue = this._value;
this._value -= item.weight;
item.resolve([previousValue, this._newReleaser(item.weight)]);
};
Semaphore.prototype._newReleaser = function (weight) {
var _this = this;
var called = false;
return function () {
if (called)
return;
called = true;
_this.release(weight);
};
};
Semaphore.prototype._drainUnlockWaiters = function () {
if (this._queue.length === 0) {
for (var weight = this._value; weight > 0; weight--) {
var waiters = this._weightedWaiters[weight - 1];
if (!waiters)
continue;
waiters.forEach(function (waiter) { return waiter.resolve(); });
this._weightedWaiters[weight - 1] = [];
}
}
else {
var queuedPriority_1 = this._queue[0].priority;
for (var weight = this._value; weight > 0; weight--) {
var waiters = this._weightedWaiters[weight - 1];
if (!waiters)
continue;
var i = waiters.findIndex(function (waiter) { return waiter.priority <= queuedPriority_1; });
(i === -1 ? waiters : waiters.splice(0, i))
.forEach((function (waiter) { return waiter.resolve(); }));
}
}
};
Semaphore.prototype._couldLockImmediately = function (weight, priority) {
return (this._queue.length === 0 || this._queue[0].priority < priority) &&
weight <= this._value;
};
return Semaphore;
}());
function insertSorted(a, v) {
var i = findIndexFromEnd(a, function (other) { return v.priority <= other.priority; });
a.splice(i + 1, 0, v);
}
function findIndexFromEnd(a, predicate) {
for (var i = a.length - 1; i >= 0; i--) {
if (predicate(a[i])) {
return i;
}
}
return -1;
}
exports["default"] = Semaphore;
/***/ }),
/***/ 586:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.E_CANCELED = exports.E_ALREADY_LOCKED = exports.E_TIMEOUT = void 0;
exports.E_TIMEOUT = new Error('timeout while waiting for mutex to become available');
exports.E_ALREADY_LOCKED = new Error('mutex already locked');
exports.E_CANCELED = new Error('request for lock canceled');
/***/ }),
/***/ 693:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.tryAcquire = exports.withTimeout = exports.Semaphore = exports.Mutex = void 0;
var tslib_1 = __webpack_require__(653);
var Mutex_1 = __webpack_require__(406);
Object.defineProperty(exports, "Mutex", ({ enumerable: true, get: function () { return Mutex_1.default; } }));
var Semaphore_1 = __webpack_require__(919);
Object.defineProperty(exports, "Semaphore", ({ enumerable: true, get: function () { return Semaphore_1.default; } }));
var withTimeout_1 = __webpack_require__(646);
Object.defineProperty(exports, "withTimeout", ({ enumerable: true, get: function () { return withTimeout_1.withTimeout; } }));
var tryAcquire_1 = __webpack_require__(746);
Object.defineProperty(exports, "tryAcquire", ({ enumerable: true, get: function () { return tryAcquire_1.tryAcquire; } }));
tslib_1.__exportStar(__webpack_require__(586), exports);
/***/ }),
/***/ 746:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.tryAcquire = void 0;
var errors_1 = __webpack_require__(586);
var withTimeout_1 = __webpack_require__(646);
// eslint-disable-next-lisne @typescript-eslint/explicit-module-boundary-types
function tryAcquire(sync, alreadyAcquiredError) {
if (alreadyAcquiredError === void 0) { alreadyAcquiredError = errors_1.E_ALREADY_LOCKED; }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (0, withTimeout_1.withTimeout)(sync, 0, alreadyAcquiredError);
}
exports.tryAcquire = tryAcquire;
/***/ }),
/***/ 646:
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.withTimeout = void 0;
var tslib_1 = __webpack_require__(653);
/* eslint-disable @typescript-eslint/no-explicit-any */
var errors_1 = __webpack_require__(586);
function withTimeout(sync, timeout, timeoutError) {
var _this = this;
if (timeoutError === void 0) { timeoutError = errors_1.E_TIMEOUT; }
return {
acquire: function (weightOrPriority, priority) {
var weight;
if (isSemaphore(sync)) {
weight = weightOrPriority;
}
else {
weight = undefined;
priority = weightOrPriority;
}
if (weight !== undefined && weight <= 0) {
throw new Error("invalid weight ".concat(weight, ": must be positive"));
}
return new Promise(function (resolve, reject) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
var isTimeout, handle, ticket, release, e_1;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
isTimeout = false;
handle = setTimeout(function () {
isTimeout = true;
reject(timeoutError);
}, timeout);
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
return [4 /*yield*/, (isSemaphore(sync)
? sync.acquire(weight, priority)
: sync.acquire(priority))];
case 2:
ticket = _a.sent();
if (isTimeout) {
release = Array.isArray(ticket) ? ticket[1] : ticket;
release();
}
else {
clearTimeout(handle);
resolve(ticket);
}
return [3 /*break*/, 4];
case 3:
e_1 = _a.sent();
if (!isTimeout) {
clearTimeout(handle);
reject(e_1);
}
return [3 /*break*/, 4];
case 4: return [2 /*return*/];
}
});
}); });
},
runExclusive: function (callback, weight, priority) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var release, ticket;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
release = function () { return undefined; };
_a.label = 1;
case 1:
_a.trys.push([1, , 7, 8]);
return [4 /*yield*/, this.acquire(weight, priority)];
case 2:
ticket = _a.sent();
if (!Array.isArray(ticket)) return [3 /*break*/, 4];
release = ticket[1];
return [4 /*yield*/, callback(ticket[0])];
case 3: return [2 /*return*/, _a.sent()];
case 4:
release = ticket;
return [4 /*yield*/, callback()];
case 5: return [2 /*return*/, _a.sent()];
case 6: return [3 /*break*/, 8];
case 7:
release();
return [7 /*endfinally*/];
case 8: return [2 /*return*/];
}
});
});
},
release: function (weight) {
sync.release(weight);
},
cancel: function () {
return sync.cancel();
},
waitForUnlock: function (weightOrPriority, priority) {
var weight;
if (isSemaphore(sync)) {
weight = weightOrPriority;
}
else {
weight = undefined;
priority = weightOrPriority;
}
if (weight !== undefined && weight <= 0) {
throw new Error("invalid weight ".concat(weight, ": must be positive"));
}
return new Promise(function (resolve, reject) {
var handle = setTimeout(function () { return reject(timeoutError); }, timeout);
(isSemaphore(sync)
? sync.waitForUnlock(weight, priority)
: sync.waitForUnlock(priority)).then(function () {
clearTimeout(handle);
resolve();
});
});
},
isLocked: function () { return sync.isLocked(); },
getValue: function () { return sync.getValue(); },
setValue: function (value) { return sync.setValue(value); },
};
}
exports.withTimeout = withTimeout;
function isSemaphore(sync) {
return sync.getValue !== undefined;
}
/***/ }),
/***/ 692:
/***/ ((module) => {
async function* asyncPool(concurrency, iterable, iteratorFn) {
const executing = new Set();
async function consume() {
const [promise, value] = await Promise.race(executing);
executing.delete(promise);
return value;
}
for (const item of iterable) {
// Wrap iteratorFn() in an async fn to ensure we get a promise.
// Then expose such promise, so it's possible to later reference and
// remove it from the executing pool.
const promise = (async () => await iteratorFn(item, iterable))().then(
value => [promise, value]
);
executing.add(promise);
if (executing.size >= concurrency) {
yield await consume();
}
}
while (executing.size) {
yield await consume();
}
}
module.exports = asyncPool;
/***/ }),
/***/ 921:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.namespace = void 0;
exports.namespace = 'lastfm-bulk-edit';
/***/ }),
/***/ 641:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createTimestampLinks = createTimestampLinks;
async function createTimestampLinks(element) {
var _a;
const libraryHref = (_a = document.querySelector('.secondary-nav-item--library a')) === null || _a === void 0 ? void 0 : _a.href;
if (!libraryHref) {
return;
}
const cells = element.querySelectorAll('.chartlist-timestamp');
for (const cell of cells) {
const span = cell.querySelector('span[title]');
if (span === null || span.parentNode !== cell) {
continue;
}
let date;
if (cell.classList.contains('chartlist-timestamp--lang-en')) {
date = new Date(Date.parse(span.title.split(',')[0]));
}
else {
// Languages other than English are not supported.
continue;
}
const dateString = getDateString(date);
const link = document.createElement('a');
link.href = `${libraryHref}?from=${dateString}&to=${dateString}`;
cell.insertBefore(link, span);
link.appendChild(span);
}
}
function getDateString(date) {
let s = date.getFullYear() + '-';
const month = date.getMonth() + 1;
if (month < 10)
s += '0';
s += month + '-';
const day = date.getDate();
if (day < 10)
s += '0';
s += day;
return s;
}
/***/ }),
/***/ 308:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.displayAlbumName = displayAlbumName;
async function displayAlbumName(element) {
var _a, _b;
const rows = element instanceof HTMLTableRowElement ? [element] : element.querySelectorAll('tr');
if (rows.length === 0) {
return;
}
const baseHref = (_a = document.querySelector('.secondary-nav-item--overview a')) === null || _a === void 0 ? void 0 : _a.getAttribute('href');
for (const row of rows) {
// Ignore non-chartlist rows.
if (!row.matches('.chartlist-row[data-edit-scrobble-id]')) {
continue;
}
// Ignore non-chartlist tables and tables with an index.
const table = row.closest('table');
if (table === null || !table.matches('.chartlist:not(.chartlist--with-index)')) {
continue;
}
// Ignore rows without a cover art image or cover art placeholder.
const coverArtAnchor = row.querySelector('.cover-art');
if (coverArtAnchor === null) {
continue;
}
// Extract album link and name from cover art and scrobble edit form.
const albumHref = coverArtAnchor.getAttribute('href');
const form = row.querySelector('form[data-edit-scrobble]:not([data-bulk-edit-scrobbles])');
let albumName;
if (form !== null) {
const formData = new FormData(form);
albumName = (_b = formData.get('album_name')) === null || _b === void 0 ? void 0 : _b.toString();
}
else {
albumName = coverArtAnchor.querySelector('img').alt;
}
// Create and insert th element.
if (!table.classList.contains('lastfm-bulk-edit-chartlist-scrobbles')) {
table.classList.add('lastfm-bulk-edit-chartlist-scrobbles');
const albumHeaderCell = document.createElement('th');
albumHeaderCell.textContent = 'Album';
const headerRow = table.tHead.rows[0];
headerRow.insertBefore(albumHeaderCell, headerRow.children[4]);
}
// Create and insert td element.
const albumCell = document.createElement('td');
albumCell.className = 'chartlist-album';
if (albumHref && albumName) {
const albumAnchor = document.createElement('a');
albumAnchor.href = albumHref;
albumAnchor.title = albumName;
albumAnchor.textContent = albumName;
albumCell.appendChild(albumAnchor);
}
else {
const noAlbumText = document.createElement('em');
noAlbumText.className = 'lastfm-bulk-edit-text-danger';
noAlbumText.textContent = 'No Album';
albumCell.appendChild(noAlbumText);
}
const nameCell = row.querySelector('.chartlist-name');
row.insertBefore(albumCell, nameCell.nextElementSibling);
// Add menu items.
if (albumHref && albumName) {
const menu = row.querySelector('.chartlist-more-menu');
const albumMenuItem1 = document.createElement('li');
const menuItemAnchor1 = document.createElement('a');
menuItemAnchor1.href = albumHref;
menuItemAnchor1.className = 'dropdown-menu-clickable-item more-item--album';
menuItemAnchor1.textContent = 'Go to album';
albumMenuItem1.appendChild(menuItemAnchor1);
const albumMenuItem2 = document.createElement('li');
const menuItemAnchor2 = document.createElement('a');
menuItemAnchor2.href = baseHref + '/library' + albumHref;
menuItemAnchor2.className = 'dropdown-menu-clickable-item more-item--album';
menuItemAnchor2.textContent = 'Go to album in library';
albumMenuItem2.appendChild(menuItemAnchor2);
const artistMenuItem = menu.querySelector('.more-item--artist').parentNode;
menu.insertBefore(albumMenuItem1, artistMenuItem);
menu.insertBefore(albumMenuItem2, artistMenuItem);
}
}
}
/***/ }),
/***/ 252:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.enhanceAutomaticEditsPage = enhanceAutomaticEditsPage;
const tiny_async_pool_1 = __importDefault(__webpack_require__(692));
const constants_1 = __webpack_require__(921);
const utils_1 = __webpack_require__(135);
const toolbarTemplate = document.createElement('template');
toolbarTemplate.innerHTML = `
Go to artist:
`;
const domParser = new DOMParser();
const artistMap = new Map();
let artistSelect = undefined;
let loadPagesPromise = undefined;
let loadPagesProgressElement = undefined;
async function enhanceAutomaticEditsPage(element) {
if (!document.URL.includes('/settings/subscription/automatic-edits')) {
return;
}
const section = element.querySelector('#subscription-corrections');
const table = section === null || section === void 0 ? void 0 : section.querySelector('table');
if (!section || !table) {
return;
}
enhanceTable(table);
const paginationList = section.querySelector('.pagination-list');
if (!paginationList) {
return;
}
const paginationListItems = [...paginationList.querySelectorAll('.pagination-page')];
const currentPageNumber = parseInt(paginationListItems.find(x => x.getAttribute('aria-current') === 'page').textContent, 10);
const pageCount = parseInt(paginationListItems[paginationListItems.length - 1].textContent, 10);
if (pageCount === 1) {
return;
}
const toolbar = toolbarTemplate.content.firstElementChild.cloneNode(true);
section.insertBefore(toolbar, section.firstElementChild);
artistSelect = toolbar.querySelector('select');
const selectedArtistKey = getSelectedArtistKey();
for (const artist of [...artistMap.values()].sort((a, b) => a.sortName.localeCompare(b.sortName))) {
const option = document.createElement('option');
option.value = artist.key;
option.selected = artist.key === selectedArtistKey;
option.text = artist.name;
const keepNothingSelected = !option.selected && artistSelect.selectedIndex === -1;
artistSelect.appendChild(option);
if (keepNothingSelected) {
artistSelect.selectedIndex = -1;
}
}
artistSelect.addEventListener('change', function () {
const selectedArtist = artistMap.get(this.value);
const anchor = document.createElement('a');
anchor.href = `?page=${selectedArtist.pageNumber}&artist=${(0, utils_1.encodeURIComponent2)(selectedArtist.name)}`;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
});
loadPagesProgressElement = document.createElement('span');
toolbar.insertAdjacentText('beforeend', ' ');
toolbar.insertAdjacentElement('beforeend', loadPagesProgressElement);
loadPagesPromise !== null && loadPagesPromise !== void 0 ? loadPagesPromise : (loadPagesPromise = loadPages(table, currentPageNumber, pageCount));
const pages = await loadPagesPromise;
toolbar.removeChild(loadPagesProgressElement);
const viewAllButton = toolbar.querySelector('button');
viewAllButton.disabled = false;
viewAllButton.addEventListener('click', async () => {
if (pages.length >= 100 && !window.confirm(`You are about to view ${pages.length} pages at once. This might take a long time to load. Are you sure?`)) {
return;
}
viewAllButton.disabled = true;
table.style.tableLayout = 'fixed';
const tableBody = table.tBodies[0];
const firstRow = tableBody.rows[0];
for (const page of pages) {
if (page.pageNumber === currentPageNumber) {
continue;
}
for (const row of page.rows) {
enhanceRow(row);
if (page.pageNumber < currentPageNumber) {
firstRow.insertAdjacentElement('beforebegin', row);
}
else {
tableBody.appendChild(row);
}
}
if (page.pageNumber % 10 === 0) {
await (0, utils_1.delay)(1);
}
}
});
}
function enhanceTable(table) {
document.body.style.backgroundColor = '#fff';
table.style.tableLayout = 'auto';
const headerRow = table.tHead.rows[0];
const body = table.tBodies[0];
let sortedCellIndex = 1;
const keys = [
'track_name_original',
'artist_name_original',
'album_name_original',
'album_artist_name_original',
];
for (let i = 0; i < 4; i++) {
const key = keys[i];
const cell = headerRow.cells[i];
cell.innerHTML = `${cell.textContent}`;
cell.addEventListener('click', () => {
const dir = sortedCellIndex === i ? -1 : 1;
sortedCellIndex = sortedCellIndex === i ? -1 : i;
const rows = [...body.rows].map(row => {
let value = row.dataset[key];
if (!value) {
value = row.querySelector(`input[name="${key}"]`).value;
row.dataset[key] = value;
}
return { row, value };
});
rows.sort((a, b) => a.value.localeCompare(b.value) * dir);
for (const row of rows) {
body.appendChild(row.row);
}
});
}
for (const row of body.rows) {
enhanceRow(row);
}
}
function enhanceRow(row) {
if (row.dataset['enhanced'] === 'true') {
return;
}
row.dataset['enhanced'] = 'true';
const formData = getFormData(row);
const trackName = formData.get('track_name').toString();
const artistName = formData.get('artist_name').toString();
const albumName = formData.get('album_name').toString();
const albumArtistName = formData.get('album_artist_name').toString();
const originalTrackName = formData.get('track_name_original').toString();
const originalArtistName = formData.get('artist_name_original').toString();
const originalAlbumName = formData.get('album_name_original').toString();
const originalAlbumArtistName = formData.get('album_artist_name_original').toString();
function emphasize(cell, content) {
var _a;
cell.style.lineHeight = '1';
cell.innerHTML = `
${cell.textContent}
${content}
Originally "${(_a = cell.textContent) === null || _a === void 0 ? void 0 : _a.trim()}"
`;
}
if (trackName !== originalTrackName) {
emphasize(row.cells[0], trackName);
}
else {
// remove bold
row.cells[0].innerHTML = row.cells[0].textContent;
}
if (artistName !== originalArtistName) {
emphasize(row.cells[1], artistName);
}
if (albumName !== originalAlbumName) {
emphasize(row.cells[2], albumName);
}
if (albumArtistName !== originalAlbumArtistName) {
emphasize(row.cells[3], albumArtistName);
}
if (originalArtistName.toLowerCase() === getSelectedArtistKey()) {
row.classList.add(`${constants_1.namespace}-highlight`);
}
}
function getFormData(row) {
return new FormData(row.querySelector('form'));
}
function getSelectedArtistKey() {
var _a;
return (_a = new URLSearchParams(location.search).get('artist')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
}
async function loadPages(table, currentPageNumber, pageCount) {
const currentPage = { pageNumber: currentPageNumber, rows: [...table.tBodies[0].rows] };
const pages = [currentPage];
const pageNumbersToLoad = [...Array(pageCount).keys()].map(i => i + 1).filter(i => i !== currentPageNumber);
addArtistsToSelect(currentPage);
updateProgressText(1, pageCount);
for await (const page of (0, tiny_async_pool_1.default)(6, pageNumbersToLoad, loadPage)) {
pages.push(page);
addArtistsToSelect(page);
updateProgressText(pages.length, pageCount);
}
pages.sort((a, b) => a.pageNumber < b.pageNumber ? -1 : 1);
return pages;
}
async function loadPage(pageNumber) {
const response = await (0, utils_1.fetchAndRetry)(`?page=${pageNumber}&_pjax=%23content`, {
credentials: 'include',
headers: {
'X-Pjax': 'true',
'X-Pjax-Container': '#content',
},
});
const text = await response.text();
const doc = domParser.parseFromString(text, 'text/html');
const table = doc.querySelector('.chart-table');
return {
pageNumber,
rows: [...table.tBodies[0].rows],
};
}
function addArtistsToSelect(page) {
const selectedArtistKey = getSelectedArtistKey();
for (const row of page.rows) {
const formData = getFormData(row);
const name = formData.get('artist_name_original').toString();
const sortName = name.replace(/\s+/g, '');
const key = name.toLowerCase();
const artist = artistMap.get(key);
if (!artist) {
artistMap.set(key, { key, name, sortName, pageNumber: page.pageNumber });
const option = document.createElement('option');
option.value = key;
option.selected = key === selectedArtistKey;
option.text = name;
const keepNothingSelected = !option.selected && artistSelect.selectedIndex === -1;
const insertAtIndex = [...artistMap.values()].sort((a, b) => a.sortName.localeCompare(b.sortName)).findIndex(x => x.key === key);
artistSelect.insertBefore(option, artistSelect.children[insertAtIndex]);
if (keepNothingSelected) {
artistSelect.selectedIndex = -1;
}
}
else if (artist.pageNumber > page.pageNumber) {
artist.pageNumber = page.pageNumber;
}
}
}
function updateProgressText(current, total) {
loadPagesProgressElement.textContent = `${current} / ${total} (${(current * 100 / total).toFixed(0)}%)`;
}
/***/ }),
/***/ 156:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const he_1 = __importDefault(__webpack_require__(488));
const constants_1 = __webpack_require__(921);
const create_timestamp_links_1 = __webpack_require__(641);
const display_album_name_1 = __webpack_require__(308);
const enhance_automatic_edits_page_1 = __webpack_require__(252);
const LoadingModal_1 = __webpack_require__(694);
const Modal_1 = __webpack_require__(946);
const utils_1 = __webpack_require__(135);
// use the top-right link to determine the current user
const authLink = document.querySelector('a.auth-link');
// https://regex101.com/r/UCmC8f/1
const albumRegExp = new RegExp(`^${authLink === null || authLink === void 0 ? void 0 : authLink.href}/library/music(/\\+[^/]*)*(/[^+][^/]*){2}$`);
const artistRegExp = new RegExp(`^${authLink === null || authLink === void 0 ? void 0 : authLink.href}/library/music(/\\+[^/]*)*(/[^+][^/]*){1}(/\\+[^/]*)?$`);
const domParser = new DOMParser();
const bulkEditScrobbleFormTemplate = document.createElement('template');
bulkEditScrobbleFormTemplate.innerHTML = `
`;
if (authLink) {
initialize();
}
function initialize() {
appendStyle();
appendBulkEditScrobblesHeaderLinkAndMenuItems(document.body);
(0, create_timestamp_links_1.createTimestampLinks)(document.body);
(0, display_album_name_1.displayAlbumName)(document.body);
(0, enhance_automatic_edits_page_1.enhanceAutomaticEditsPage)(document.body);
// use MutationObserver because Last.fm is a single-page application
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
if (node.hasAttribute('data-processed')) {
continue;
}
node.setAttribute('data-processed', 'true');
appendBulkEditScrobblesHeaderLinkAndMenuItems(node);
(0, create_timestamp_links_1.createTimestampLinks)(document.body);
(0, display_album_name_1.displayAlbumName)(node);
(0, enhance_automatic_edits_page_1.enhanceAutomaticEditsPage)(node);
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
function appendStyle() {
const style = document.createElement('style');
style.innerHTML = `
.${constants_1.namespace}-title[title] {
cursor: help !important;
}
@media (pointer: coarse), (hover: none) {
.${constants_1.namespace}-title[title]:focus {
position: relative;
display: inline-flex;
justify-content: center;
}
.${constants_1.namespace}-title[title]:focus::after {
content: attr(title);
position: absolute;
top: 100%;
left: 0%;
color: #fff;
background-color: #2b2a32;
border: 1px solid #fff;
width: fit-content;
padding: 4px 7px;
font-size: small;
line-height: normal;
white-space: pre;
z-index: 1;
}
}
.${constants_1.namespace}-ellipsis {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.${constants_1.namespace}-form-group-controls {
margin-left: 0 !important;
}
.${constants_1.namespace}-list {
column-count: 2;
}
.${constants_1.namespace}-loading {
background: url("/static/images/loading_dark_light_64.gif") 50% 50% no-repeat;
height: 64px;
display: flex;
justify-content: center;
align-items: center;
}
.${constants_1.namespace}-text-danger {
color: #d92323;
}
.${constants_1.namespace}-text-info {
color: #2b65d9;
}
@media (min-width: 768px) {
.${constants_1.namespace}-chartlist-scrobbles .chartlist-name {
margin-top: -2px;
margin-bottom: 13px;
}
.${constants_1.namespace}-chartlist-scrobbles .chartlist-album {
margin-top: 13px;
margin-bottom: -2px;
position: absolute;
left: 133.5px;
width: 182.41px;
}
.${constants_1.namespace}-chartlist-scrobbles .chartlist-album::before {
width: 0 !important;
}
}
@media (min-width: 1260px) {
.${constants_1.namespace}-chartlist-scrobbles .chartlist-album {
width: 272.41px;
}
}
.${constants_1.namespace}-highlight {
background-color: #fff9e5;
}
.${constants_1.namespace}-highlight:hover {
background-color: #fcf2cf !important;
}`;
document.head.appendChild(style);
}
function appendBulkEditScrobblesHeaderLinkAndMenuItems(element) {
if (!document.URL.startsWith(authLink.href)) {
return; // current page is not the user's profile
}
appendBulkEditScrobblesHeaderLink(element);
appendBulkEditScrobblesMenuItems(element);
}
function appendBulkEditScrobblesHeaderLink(element) {
var _a;
const header = element.querySelector('.library-header');
if (header === null) {
return; // current page does not contain the header we're looking for
}
const { form, click } = getBulkEditScrobbleMenuItem(document.URL);
const link = document.createElement('a');
link.href = 'javascript:void(0)';
link.textContent = 'Bulk edit scrobbles';
link.addEventListener('click', click);
if (((_a = header.lastElementChild) === null || _a === void 0 ? void 0 : _a.tagName) !== 'H2') {
header.insertAdjacentText('beforeend', ' ยท ');
}
header.insertAdjacentElement('beforeend', link);
header.insertAdjacentElement('beforeend', form);
}
function appendBulkEditScrobblesMenuItems(element) {
const rows = element instanceof HTMLTableRowElement ? [element] : element.querySelectorAll('tr');
for (const row of rows) {
const link = row.querySelector('a.chartlist-count-bar-link,a.more-item--track[href*="/user/"]');
if (!link) {
continue; // this is not an artist, album or track
}
const { form, click } = getBulkEditScrobbleMenuItem(link.href, row);
const button = document.createElement('button');
button.className = 'mimic-link dropdown-menu-clickable-item more-item--edit-old';
button.textContent = 'Bulk edit scrobbles';
button.setAttribute('data-analytics-action', 'BulkEditScrobblesOpen');
button.addEventListener('click', click);
form.style.marginTop = '0';
const bulkEditScrobbleMenuItem = document.createElement('li');
bulkEditScrobbleMenuItem.appendChild(button);
bulkEditScrobbleMenuItem.appendChild(form);
bulkEditScrobbleMenuItem.setAttribute('data-processed', 'true');
// insert/replace "Bulk edit scrobbles" menu item so it comes after "Edit scrobble"
const menu = row.querySelector('.chartlist-more-menu');
let editScrobbleMenuItem = undefined;
for (const menuItem of menu.children) {
if (menuItem.hasAttribute('data-processed')) {
menu.removeChild(menuItem);
}
else if (menuItem.querySelector('button.more-item--edit-old') !== null) {
editScrobbleMenuItem = menuItem;
}
}
if (editScrobbleMenuItem) {
menu.insertBefore(bulkEditScrobbleMenuItem, editScrobbleMenuItem.nextElementSibling);
}
else {
menu.insertBefore(bulkEditScrobbleMenuItem, menu.firstElementChild);
}
}
}
function getBulkEditScrobbleMenuItem(url, row) {
const urlType = getUrlType(url);
const form = bulkEditScrobbleFormTemplate.content.firstElementChild.cloneNode(true);
const submitButton = form.querySelector('button');
let allScrobbleData;
let scrobbleData;
const click = async () => {
if (!allScrobbleData) {
const loadingModal = createLoadingModal('Loading Scrobbles...', { dismissible: true, display: 'percentage' });
try {
allScrobbleData = await fetchScrobbleData(url, loadingModal, loadingModal);
if (!loadingModal.isAttached) {
return;
}
}
finally {
loadingModal.hide();
}
}
scrobbleData = allScrobbleData;
// use JSON strings as album keys to uniquely identify combinations of album + album artists
// group scrobbles by album key
let scrobbleDataGroups = [...groupBy(allScrobbleData, (s) => {
var _a, _b;
return JSON.stringify({
album_name: (_a = s.get('album_name')) !== null && _a !== void 0 ? _a : '',
album_artist_name: (_b = s.get('album_artist_name')) !== null && _b !== void 0 ? _b : '',
});
})];
// sort groups by the amount of scrobbles
scrobbleDataGroups = scrobbleDataGroups.sort(([_key1, values1], [_key2, values2]) => values2.length - values1.length);
// when editing multiple albums album, show an album selection dialog first
if (scrobbleDataGroups.length >= 2) {
const noAlbumKey = JSON.stringify({ album_name: '', album_artist_name: '' });
let currentAlbumKey = undefined;
// put the "No Album" album first
scrobbleDataGroups = scrobbleDataGroups.sort(([key1], [key2]) => {
if (key1 === noAlbumKey)
return -1;
if (key2 === noAlbumKey)
return +1;
return 0;
});
// when the edit dialog was initiated from an album or album track, put that album first in the list
if (urlType === 'album' || getUrlType(document.URL) === 'album') {
// grab the current album name and artist name from the DOM
const album_name = (urlType === 'album' && row
? row.querySelector('.chartlist-name')
: document.querySelector('.library-header-title')).textContent.trim();
const album_artist_name = (urlType === 'album' && row
? row.querySelector('.chartlist-artist') || document.querySelector('.library-header-title, .library-header-crumb')
: document.querySelector('.text-colour-link')).textContent.trim();
currentAlbumKey = JSON.stringify({ album_name, album_artist_name });
// put the current album first
scrobbleDataGroups = scrobbleDataGroups.sort(([key1], [key2]) => {
if (key1 === currentAlbumKey)
return -1;
if (key2 === currentAlbumKey)
return +1;
if (key1 === noAlbumKey)
return -1;
if (key2 === noAlbumKey)
return +1;
return 0;
});
}
const body = document.createElement('div');
body.innerHTML = `
${urlType === 'track' ? 'This track is' : `Tracks from this ${urlType} are`} scrobbled under multiple albums.
Select which albums you would like to edit.
Deselect albums you would like to skip.