Skip to content

Instantly share code, notes, and snippets.

@luejerry
Last active August 3, 2017 02:02
Show Gist options
  • Save luejerry/16f1e85464af4af2e6cba40d6ca92e34 to your computer and use it in GitHub Desktop.
Save luejerry/16f1e85464af4af2e6cba40d6ca92e34 to your computer and use it in GitHub Desktop.
Userscript to mark already read chapters on Dynasty-Scans
// ==UserScript==
// @name Dynasty Mark IsRead
// @author cyricc
// @description Mark chapters that you have already read in Dynasty Scans chapter lists.
// @namespace https://dynasty-scans.com
// @include https://dynasty-scans.com/chapters/added*
// @include https://dynasty-scans.com/tags/*
// @include https://dynasty-scans.com/issues/*
// @include https://dynasty-scans.com/doujins/*
// @include https://dynasty-scans.com/anthologies/*
// @include https://dynasty-scans.com/series/*
// @include https://dynasty-scans.com/authors/*
// @include https://dynasty-scans.com/pairings/*
// @include https://dynasty-scans.com/scanlators/*
// @include https://dynasty-scans.com/search*
// @include https://dynasty-scans.com/
// @include https://dynasty-scans.com/?*
// @version 1.9
// @grant none
// ==/UserScript==
(function () {
console.log("Running Dynasty-IsRead user script.");
const timeStart = performance.now();
const listHref = "https://dynasty-scans.com/lists";
/* Minimum number of chapters on page needed to trigger a batch fetch */
const entryThreshold = 30;
/* Promisify XMLHttpRequest */
const httpGet = function (url) {
return new Promise((resolve, reject) => {
const xhttp = new XMLHttpRequest();
xhttp.onload = () => {
if (xhttp.status == 200) {
resolve(xhttp.responseXML);
} else {
reject(Error(xhttp.statusText));
}
};
xhttp.open("GET", url);
xhttp.responseType = "document";
xhttp.send();
});
};
/* Check if an individual chapter is Read */
const scrapeIsRead = function (htmlDocument) {
var dropList = htmlDocument.getElementById("lists-dropdown").children;
var readElements = Array.from(dropList)
.map(e => e.children[0])
.filter(a => a.getAttribute("data-type") === "read")
.filter(a => a.getElementsByClassName("icon-remove").length > 0);
return readElements.length > 0;
};
/* Apply style to Read link or caption */
const formatIsRead = function (element) {
element.style.color = "#999999";
};
/* Check and mark an individual link if Read */
const markLinkIsRead = function (a) {
return httpGet(a.href).then(responseHtml => {
if (scrapeIsRead(responseHtml)) {
formatIsRead(a);
}
return Promise.resolve();
});
};
/* Check and mark an individual thumbnail caption if Read */
const markThumbnailIsRead = function (a) {
const titleDiv = a.getElementsByClassName("title")[0] || a.getElementsByClassName("caption")[0];
return httpGet(a.href).then(responseHtml => {
if (scrapeIsRead(responseHtml)) {
formatIsRead(titleDiv);
}
return Promise.resolve();
});
};
/* Promise to fetch the user's Read list and return it as a lookup table */
const promiseIsReadMap = function(isReadHref) {
return httpGet(isReadHref).then(responseHtml => {
const isReadList = responseHtml.getElementsByTagName("dd");
return Array.from(isReadList)
.map(dd => dd.getElementsByClassName("name")[0])
.filter(a => a !== undefined)
.reduce((acc, a) => { acc[a.href] = true; return acc; }, {});
});
};
const promiseMarkIndividualLinks = function(entryLinks, thumbnailLinks) {
const markLinkPromises = entryLinks.map(a => markLinkIsRead(a));
const markThumbnailPromises = thumbnailLinks.map(a => markThumbnailIsRead(a));
return Promise.all([...markLinkPromises, ...markThumbnailPromises]);
};
/* Main */
// Find all links to chapters on the page
const entryList = document.getElementsByTagName("dd");
const entryLinks = Array.from(entryList)
.map(dd => dd.getElementsByClassName("name")[0])
.filter(a => a !== undefined);
const thumbnailList = Array.from(document.getElementsByClassName("thumbnail"));
const thumbnailLinks = thumbnailList
.filter(e => e.tagName === "A")
.filter(a => a.getElementsByClassName("title")[0] || a.getElementsByClassName("caption")[0]);
const numLinks = entryLinks.length + thumbnailLinks.length;
// Run algorithm based on the number of chapter links found.
// Below a certain threshold, it is faster to scrape Read status from each individual chapter page.
// Above that threshold, it is faster to request the user's entire Read list.
// If above threshold, both methods are used concurrently to improve perceived responsiveness.
if (numLinks < entryThreshold) {
console.log(`Dynasty-IsRead: Less than ${entryThreshold} chapters on page, using serial fetch only`);
promiseMarkIndividualLinks(entryLinks, thumbnailLinks).then(promises => {
const timeDeltaMillis = performance.now() - timeStart;
console.log(`Dynasty-IsRead: finished marking ${promises.length} chapters in ${timeDeltaMillis.toFixed()} ms.`);
}).catch(error => console.log(`Dynasty-IsRead: ${error.name} occurred during marking: ${error.message}`));
} else {
console.log(`Dynasty-IsRead: ${numLinks} chapters, using hybrid batch fetch`);
httpGet(listHref).then(responseHtml => {
// GET the user's Read list (the url is different for each user)
const listLinks = responseHtml.getElementsByClassName("table-link");
const isReadHref = Array.from(listLinks).find(a => a.innerText === "Read").href;
const fetchIsReadMap = promiseIsReadMap(isReadHref);
// Start checking and marking individual chapters while waiting on Read list
promiseMarkIndividualLinks(entryLinks.slice(0, entryThreshold), thumbnailLinks.slice(0, entryThreshold)).then(promises => {
const timeDeltaMillis = performance.now() - timeStart;
console.log(`Dynasty-IsRead: finished pre-marking ${promises.length} chapters in ${timeDeltaMillis.toFixed()} ms.`);
}).catch(error => console.log(`Dynasty-IsRead: ${error.name} occurred during pre-marking: ${error.message}`));
return fetchIsReadMap;
}).then(isReadMap => {
// Mark chapters on page that are Read
entryLinks
.filter(a => isReadMap[a.href])
.forEach(a => formatIsRead(a));
thumbnailLinks
.filter(a => isReadMap[a.href])
.map(a => a.getElementsByClassName("title")[0] || a.getElementsByClassName("caption")[0])
.filter(div => div !== undefined)
.forEach(div => formatIsRead(div));
return Promise.resolve();
}).then(() => {
const timeDeltaMillis = performance.now() - timeStart;
console.log(`Dynasty-IsRead: finished marking ${numLinks} chapters in ${timeDeltaMillis.toFixed()} ms.`);
}).catch(error => {
console.log(`Dynasty-IsRead: ${error.name} occurred during batch fetch: ${error.message}`);
});
}
})();
@luejerry
Copy link
Author

luejerry commented Jul 28, 2017

Read chapters are marked grey:

dynastyscript

Added in version 1.5:

userscript2

@luejerry
Copy link
Author

luejerry commented Jul 29, 2017

Changelog

  • 1.9: output performance stats to console. Refactor to better adhere to ES6 best practices.
  • 1.8: improved performance by skipping checking links with no text (e.g. image links).
  • 1.7: refactoring for clarity. No functionality changes.
  • 1.6: significant performance enhancement, especially on short lists.
  • 1.5: now also works on chapters shown as thumbnails as well (e.g. on the main page).
  • 1.4: slight optimizations.
  • 1.3: null handling and code cleanup.
  • 1.2: now applies to search results pages.
  • 1.1: significant performance enhancement on large lists.
  • 1.0: initial release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment