neo-scraper/src/engines/Moebooru.ts

134 lines
4.6 KiB
TypeScript

import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine.js";
import { TagCategory } from "../BooruTypes.js";
import { createNotesFromMoebooruBoxes, guessContentType, parseResolutionString } from "../Utility.js";
export default class Moebooru extends ScrapeEngineBase {
name = "moebooru";
features: ScrapeEngineFeature[] = ["content", "rating", "resolution", "tags", "tag_category", "source", "notes"];
notes = [];
supportedHosts = ["yande.re", "konachan.com", "konachan.net", "www.sakugabooru.com", "img.genshiken-itb.org"];
scrapeDocument(document: Document): ScrapeResult {
const result = new ScrapeResult(this.name);
const post = new ScrapedPost();
post.pageUrl = document.location.href;
if (document.location.host == "yande.re") {
post.uploadMode = "content";
}
// Set image url
const originalImageElement = document.querySelector("#highres") as HTMLAnchorElement;
if (originalImageElement) {
post.contentUrl = originalImageElement.href;
} else {
// No point in continuing when we don't have an image.
return result;
}
// Set content type
post.contentType = guessContentType(post.contentUrl);
// Set rating and source
// Same method as Gelbooru v025.
const regex = new RegExp("(.*): (.*)");
const statsElements = Array.from(document.querySelectorAll("#stats > ul > li")).map((x) => x as HTMLLIElement);
for (const el of statsElements) {
const matches = el.innerText.match(regex);
if (matches && matches.length == 3) {
// If Source element
if (matches[1] == "Source" && el.children.length > 0) {
post.sources = [(el.children[0] as HTMLAnchorElement).href];
}
// If Rating element
else if (matches[1] == "Rating") {
switch (matches[2].toLowerCase()) {
case "safe":
post.rating = "safe";
break;
case "questionable":
post.rating = "sketchy";
break;
case "explicit":
post.rating = "unsafe";
break;
}
}
// If size element
else if (matches[1] == "Size") {
post.resolution = parseResolutionString(matches[2]);
}
}
}
// Set tags
const tagElements = Array.from(document.querySelectorAll("#tag-sidebar > li")).map((x) => x as HTMLLIElement);
for (const el of tagElements) {
let tagName: string;
const tagNameElements = el.getElementsByTagName("a");
if (tagNameElements.length > 1) {
// First <a> is "?"
// (opt) Second <a> is "+" (when logged in)
// (opt) Third <a> is "-" (when logged in)
// Last <a> is the actual tag name
tagName = tagNameElements[tagNameElements.length - 1].innerText;
} else {
// Skip if no tag name is found
continue;
}
let category: TagCategory | undefined;
// Loop over all classes because some sites (konachan)
// have mutliple classes, e.g. "tag-link tag-type-copyright"
for (const className of Array.from(el.classList)) {
switch (className) {
case "tag-type-copyright":
category = "copyright";
break;
case "tag-type-character":
category = "character";
break;
case "tag-type-artist":
category = "artist";
break;
case "tag-type-metadata":
category = "meta";
break;
}
}
const tag = new ScrapedTag(tagName, category);
post.tags.push(tag);
}
// Set notes
const noteBoxSizeEl = document.getElementById("image") as HTMLImageElement;
if (!noteBoxSizeEl) {
this.log("noteBoxSizeEl is undefined.");
} else {
const boxSize: [number, number] = [noteBoxSizeEl.width, noteBoxSizeEl.height];
post.notes = createNotesFromMoebooruBoxes(document, boxSize);
}
// Sometimes there is both a JPEG and a PNG version of a post.
// If this is the case, copy all the information from the JPEG
// version and only replace the contentUrl with the link to the PNG version.
// We want to add this PNG version before the other version, so that it appears on top.
const unchangedImgEl = document.querySelector<HTMLAnchorElement>("a.original-file-unchanged");
if (unchangedImgEl && unchangedImgEl.href != post.contentUrl) {
const copy = Object.assign(new ScrapedPost(), post);
copy.contentUrl = unchangedImgEl.href;
result.tryAddPost(copy);
}
// Add normal/non-png post.
result.tryAddPost(post);
return result;
}
}