Skip to content

Instantly share code, notes, and snippets.

@Klaster1
Last active October 3, 2023 12:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Klaster1/11a1e873db7dd6d2e5224dca3ebcfe47 to your computer and use it in GitHub Desktop.
Save Klaster1/11a1e873db7dd6d2e5224dca3ebcfe47 to your computer and use it in GitHub Desktop.
MangaUpdates select scanlated series userscript
// ==UserScript==
// @name MangaUpdates scanlated selector
// @author Klaster_1
// @version 1.0.2
// @match https://www.mangaupdates.com/mylist.html*
// @description Checks scanlated series from reading list. Useful to manage wishlists.
// @icon https://www.mangaupdates.com/favicon.ico
// @downloadURL https://gist.github.com/Klaster1/11a1e873db7dd6d2e5224dca3ebcfe47/raw/mu-select-scanlated.user.js
// @updateURL https://gist.github.com/Klaster1/11a1e873db7dd6d2e5224dca3ebcfe47/raw/mu-select-scanlated.user.js
// @grant none
// ==/UserScript==
const asyncWait = (duration) =>
new Promise((resolve) => setTimeout(() => resolve(true), duration));
const retry = async (fn, attempts) => {
try {
return await fn();
} catch (e) {
if (attempts === 0) {
return Promise.reject(e);
} else {
console.log(`Retrying after ${3_000 / attempts}ms`);
await asyncWait(3_000 / attempts);
return retry(fn, attempts - 1);
}
}
};
const gatherSeries = () =>
Array.from(document.querySelectorAll(`a[href*='/series/'`), (a) => ({
url: a.href,
checkbox: a.closest(".row").querySelector("input[type=checkbox]"),
name: a.innerText,
}));
const querySection =
({ sectionTitle, partialContentText }) =>
(text) =>
[
...document
.createRange()
.createContextualFragment(text)
.querySelectorAll(".sCat"),
]
.filter((el) => el.innerText.includes(sectionTitle))
.map((el) => el.nextElementSibling.innerText.includes(partialContentText))
.pop();
const splitArray = (arr, size) =>
arr.reduce(
(a, v, i) =>
a[a.length - 1].length < size
? [...a.slice(0, -1), [...a[a.length - 1], v]]
: [...a, [v]],
[[]]
);
const asyncMap = (fn, data = [], concurrency = 3) =>
Promise.all(
splitArray(data, concurrency).map((chunk) =>
chunk.reduce((a, b) => a.then(() => fn(b)), Promise.resolve())
)
);
const getSeriesPage = (url) =>
retry(
() =>
fetch(url)
.then((res) => res.text())
.then((text) => {
if (text.includes("503 Service Temporarily Unavailable")) {
return Promise.reject("Got a 503");
} else {
return text;
}
}),
4
).catch((e) => console.log(url, e));
const selectSeries = ({
predicate,
onResult = () => {},
onComplete = () => {},
concurrency = 3,
} = {}) => {
const allSeries = gatherSeries();
asyncMap(
(series) =>
getSeriesPage(series.url)
.then(predicate)
.then((matches) => {
series.checkbox.checked = matches;
series.matches = matches;
onResult(allSeries, series, matches);
}),
allSeries
).then(() => onComplete(allSeries));
};
const injectButton = ({ label, predicate }) => {
const button = document
.createRange()
.createContextualFragment(
`
<button type='button' class='button'>☑ ${label}</button>
`
)
.querySelector("button");
button.style.marginLeft = "5px";
let done = 0;
button.onclick = (e) =>
selectSeries({
predicate,
concurrency: 10,
onResult(all, item, value) {
done += 1;
e.target.innerText = `☑ ${label} (${done} / ${all.length})`;
},
onComplete(all) {
done = 0;
e.target.innerText = `☑ ${label}`;
},
});
document.querySelector('button[value="Add Series"]').after(button);
};
injectButton({
label: `scanlated`,
predicate: querySection({
sectionTitle: "Completely Scanlated?",
partialContentText: "Yes",
}),
});
injectButton({
label: `completed`,
predicate: querySection({
sectionTitle: "Status in Country of Origin",
partialContentText: "(Complete)",
}),
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment