Add feature info to engines + project cleanup
parent
198da89bc4
commit
a599710276
|
|
@ -0,0 +1,17 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
commonjs: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
},
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
rules: {
|
||||||
|
semi: ["error", "always"],
|
||||||
|
},
|
||||||
|
};
|
||||||
21
README.md
21
README.md
|
|
@ -1,3 +1,24 @@
|
||||||
# neo-scraper
|
# neo-scraper
|
||||||
|
|
||||||
Scrape a booru-like post object from the DOM.
|
Scrape a booru-like post object from the DOM.
|
||||||
|
|
||||||
|
## Supported sites
|
||||||
|
|
||||||
|
| Engine | Sites | Features | Notes |
|
||||||
|
| -------------- | ------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------- |
|
||||||
|
| animepictures | anime-pictures.net | content, resolution, tags, tag_category | Rating is assumed to be safe. |
|
||||||
|
| danbooru | danbooru.donmai.us, safebooru.donmai.us | content, rating, resolution, tags, tag_category, source, notes | |
|
||||||
|
| e621 | e621.net, e926.net | content, rating, resolution, tags, tag_category, source | |
|
||||||
|
| furaffinity | www.furaffinity.net | content, rating, resolution, tags, tag_category | |
|
||||||
|
| gelbooru | safebooru.org, gelbooru.com, rule34.xxx, tbib.org, xbooru.com | content, rating, resolution, tags, tag_category, source, notes | Supported features might vary between hosts. |
|
||||||
|
| inkbunny | inkbunny.net | content, rating, resolution, tags | |
|
||||||
|
| moebooru | yande.re, konachan.com | content, rating, resolution, tags, tag_category, source, notes | |
|
||||||
|
| pixiv | pixiv.net, www.pixiv.net | content | |
|
||||||
|
| reddit | reddit.com, new.reddit.com, old.reddit.com | content | |
|
||||||
|
| rule34us | rule34.us | content, resolution, tags, tag_category | Rating is assumed to be unsafe. |
|
||||||
|
| sankakucomplex | chan.sankakucomplex.com | content, rating, tags, tag_category, notes | |
|
||||||
|
| shimmie2 | rule34.paheal.net, rule34hentai.net | content, tags, source | Rating is assumed to be unsafe. |
|
||||||
|
| shuushuu | e-shuushuu.net | content, rating, resolution, tags, tag_category | Rating is assumed to be safe. |
|
||||||
|
| twitter | twitter.com, mobile.twitter.com | content | |
|
||||||
|
| zerochan | www.zerochan.net | content, resolution, tags, tag_category | Rating is assumed to be safe. |
|
||||||
|
| fallback | | content | Tries to find the largest image or video on the current page. |
|
||||||
|
|
|
||||||
17
package.json
17
package.json
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "neo-scraper",
|
"name": "neo-scraper",
|
||||||
"version": "0.5.4",
|
"version": "0.5.5",
|
||||||
"description": "Scrape a booru-like post object from the DOM.",
|
"description": "Scrape a booru-like post object from the DOM.",
|
||||||
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.js",
|
"types": "dist/index.d.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -10,7 +11,9 @@
|
||||||
"test:debug": "cross-env HEADLESS=false jest --testTimeout=100000000",
|
"test:debug": "cross-env HEADLESS=false jest --testTimeout=100000000",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
"prepare": "npm run build"
|
"prepare": "npm run build",
|
||||||
|
"lint": "eslint src scripts",
|
||||||
|
"summary-table": "npm run build && node --experimental-specifier-resolution=node --loader ts-node/esm scripts/summary-table.ts"
|
||||||
},
|
},
|
||||||
"author": "neobooru",
|
"author": "neobooru",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -24,15 +27,23 @@
|
||||||
"@types/jsdom": "^12.2.4",
|
"@types/jsdom": "^12.2.4",
|
||||||
"@types/puppeteer": "^5.4.4",
|
"@types/puppeteer": "^5.4.4",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
||||||
|
"@typescript-eslint/parser": "^5.32.0",
|
||||||
|
"eslint": "8.7.0",
|
||||||
|
"eslint-plugin-import": "^2.25.2",
|
||||||
|
"eslint-plugin-n": "^15.0.0",
|
||||||
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-puppeteer": "^6.0.0",
|
"jest-puppeteer": "^6.0.0",
|
||||||
"jsdom": "^15.2.1",
|
"jsdom": "^15.2.1",
|
||||||
|
"prettier": "2.5.1",
|
||||||
"puppeteer": "^13.1.2",
|
"puppeteer": "^13.1.2",
|
||||||
"ts-jest": "^26.5.6",
|
"ts-jest": "^26.5.6",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.5.2"
|
"typescript": "^4.5.2"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "./jest.preset.js",
|
"preset": "./jest.preset.cjs",
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.tsx?$": "ts-jest"
|
"^.+\\.tsx?$": "ts-jest"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import NeoScraper from "../src/NeoScraper";
|
||||||
|
import FallbackEngine from "../src/engines/Fallback";
|
||||||
|
|
||||||
|
const engines = [...new NeoScraper().engines, new FallbackEngine()];
|
||||||
|
const txt = ["| Engine | Sites | Features | Notes |", "|---|---|---|---|---|"];
|
||||||
|
|
||||||
|
for (const engine of engines) {
|
||||||
|
txt.push(
|
||||||
|
`| ${engine.name} | ${engine.supportedHosts.join(", ")} | ${engine.features.join(", ")} | ${engine.notes.join(
|
||||||
|
" "
|
||||||
|
)} |`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(txt.join("\n"));
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import NeoScraper from "./NeoScraper";
|
import NeoScraper from "./NeoScraper";
|
||||||
|
|
||||||
test("create scraper with default engines", async () => {
|
test("create scraper with default engines", async () => {
|
||||||
const scraper = new NeoScraper();
|
new NeoScraper();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("all default scrapers have unique names", () => {
|
test("all default scrapers have unique names", () => {
|
||||||
const scraper = new NeoScraper();
|
const scraper = new NeoScraper();
|
||||||
let alreadySeen: string[] = [];
|
const alreadySeen: string[] = [];
|
||||||
|
|
||||||
for (const engine of scraper.engines) {
|
for (const engine of scraper.engines) {
|
||||||
if (alreadySeen.indexOf(engine.name) != -1) {
|
if (alreadySeen.indexOf(engine.name) != -1) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export default class NeoScraper {
|
||||||
engines: ScrapeEngine[] = [];
|
engines: ScrapeEngine[] = [];
|
||||||
fallbackEngine: ScrapeEngine | null = null;
|
fallbackEngine: ScrapeEngine | null = null;
|
||||||
|
|
||||||
constructor(useDefaultEngines: boolean = true) {
|
constructor(useDefaultEngines = true) {
|
||||||
if (useDefaultEngines) {
|
if (useDefaultEngines) {
|
||||||
this.engines.push(
|
this.engines.push(
|
||||||
new engines.AnimePictures(),
|
new engines.AnimePictures(),
|
||||||
|
|
@ -28,12 +28,7 @@ export default class NeoScraper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrapeDocument(
|
scrapeDocument(document: Document, allowFallbackEngine = false, ignoreCanImport = false): ScrapeResults {
|
||||||
document: Document,
|
|
||||||
engines: string[] | null = null,
|
|
||||||
allowFallbackEngine: boolean = false,
|
|
||||||
ignoreCanImport: boolean = false
|
|
||||||
): ScrapeResults {
|
|
||||||
const res = new ScrapeResults();
|
const res = new ScrapeResults();
|
||||||
|
|
||||||
for (const engine of this.engines) {
|
for (const engine of this.engines) {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { TagCategory, SafetyRating } from "./BooruTypes";
|
||||||
export type ContentType = "image" | "video";
|
export type ContentType = "image" | "video";
|
||||||
|
|
||||||
export class ScrapedPost {
|
export class ScrapedPost {
|
||||||
contentUrl: string = "";
|
contentUrl = "";
|
||||||
pageUrl: string = "";
|
pageUrl = "";
|
||||||
contentType: ContentType = "image";
|
contentType: ContentType = "image";
|
||||||
resolution: [number, number] | undefined;
|
resolution: [number, number] | undefined;
|
||||||
rating: SafetyRating = "safe";
|
rating: SafetyRating = "safe";
|
||||||
|
|
@ -30,7 +30,7 @@ export class ScrapedNote {
|
||||||
|
|
||||||
export class ScrapeResult {
|
export class ScrapeResult {
|
||||||
engine: string;
|
engine: string;
|
||||||
description: string = "";
|
description = "";
|
||||||
posts: ScrapedPost[] = [];
|
posts: ScrapedPost[] = [];
|
||||||
|
|
||||||
constructor(engine: string) {
|
constructor(engine: string) {
|
||||||
|
|
@ -50,9 +50,9 @@ export class ScrapeResults {
|
||||||
results: ScrapeResult[] = [];
|
results: ScrapeResult[] = [];
|
||||||
|
|
||||||
get posts(): ScrapedPost[] {
|
get posts(): ScrapedPost[] {
|
||||||
var posts: ScrapedPost[] = [];
|
let posts: ScrapedPost[] = [];
|
||||||
|
|
||||||
for (var res of this.results) {
|
for (const res of this.results) {
|
||||||
posts = posts.concat(res.posts);
|
posts = posts.concat(res.posts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,15 +60,28 @@ export class ScrapeResults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ScrapeEngineFeature = "content" | "rating" | "resolution" | "tags" | "tag_category" | "source" | "notes";
|
||||||
|
|
||||||
export interface ScrapeEngine {
|
export interface ScrapeEngine {
|
||||||
name: string;
|
name: string;
|
||||||
|
features: ScrapeEngineFeature[];
|
||||||
|
notes: string[];
|
||||||
|
supportedHosts: string[];
|
||||||
|
|
||||||
canImport(url: Location): boolean;
|
canImport(url: Location): boolean;
|
||||||
scrapeDocument(document: Document): ScrapeResult;
|
scrapeDocument(document: Document): ScrapeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class ScrapeEngineBase implements ScrapeEngine {
|
export abstract class ScrapeEngineBase implements ScrapeEngine {
|
||||||
abstract name: string;
|
abstract name: string;
|
||||||
abstract canImport(url: Location): boolean;
|
abstract features: ScrapeEngineFeature[];
|
||||||
|
abstract notes: string[];
|
||||||
|
abstract supportedHosts: string[];
|
||||||
|
|
||||||
|
canImport(url: Location): boolean {
|
||||||
|
return this.supportedHosts.indexOf(url.host) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
abstract scrapeDocument(document: Document): ScrapeResult;
|
abstract scrapeDocument(document: Document): ScrapeResult;
|
||||||
|
|
||||||
protected log(str: string) {
|
protected log(str: string) {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export function getUrlExtension(url: string) {
|
||||||
* @param url
|
* @param url
|
||||||
* @returns "image" | "video"
|
* @returns "image" | "video"
|
||||||
*/
|
*/
|
||||||
export function guessContentType(url: string): ContentType {
|
export function guessContentType(url: string | undefined): ContentType {
|
||||||
if (url != undefined) {
|
if (url != undefined) {
|
||||||
const ext = getUrlExtension(url);
|
const ext = getUrlExtension(url);
|
||||||
if (videoExtensions.indexOf(ext) != -1) return "video";
|
if (videoExtensions.indexOf(ext) != -1) return "video";
|
||||||
|
|
@ -109,7 +109,7 @@ export function createNoteFromDanbooruArticle(post: ScrapedPost, el: HTMLElement
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const num = parseInt(value);
|
const num = parseInt(value);
|
||||||
if (num == NaN) {
|
if (isNaN(num)) {
|
||||||
logFail(`parseInt on '${value}' returned NaN.`);
|
logFail(`parseInt on '${value}' returned NaN.`);
|
||||||
}
|
}
|
||||||
return num;
|
return num;
|
||||||
|
|
@ -132,10 +132,10 @@ export function createNoteFromDanbooruArticle(post: ScrapedPost, el: HTMLElement
|
||||||
text = htmlNoteToMarkdownNote(text);
|
text = htmlNoteToMarkdownNote(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
let x = getInt(data, "x");
|
const x = getInt(data, "x");
|
||||||
let y = getInt(data, "y");
|
const y = getInt(data, "y");
|
||||||
let w = getInt(data, "width");
|
const w = getInt(data, "width");
|
||||||
let h = getInt(data, "height");
|
const h = getInt(data, "height");
|
||||||
|
|
||||||
if (!text || !x || !y || !w || !h) {
|
if (!text || !x || !y || !w || !h) {
|
||||||
logFail("A required data field could not be parsed.");
|
logFail("A required data field could not be parsed.");
|
||||||
|
|
@ -159,7 +159,7 @@ export function createNoteFromDanbooruArticle(post: ScrapedPost, el: HTMLElement
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function createNotesFromMoebooruBoxes(document: Document, boxSize: [number, number]) {
|
export function createNotesFromMoebooruBoxes(document: Document, boxSize: [number, number]) {
|
||||||
let notes: ScrapedNote[] = [];
|
const notes: ScrapedNote[] = [];
|
||||||
const noteBoxes = Array.from(document.querySelectorAll(".note-box")).map((x) => x as HTMLLIElement);
|
const noteBoxes = Array.from(document.querySelectorAll(".note-box")).map((x) => x as HTMLLIElement);
|
||||||
const noteBodies = Array.from(document.querySelectorAll(".note-body")).map((x) => x as HTMLLIElement);
|
const noteBodies = Array.from(document.querySelectorAll(".note-body")).map((x) => x as HTMLLIElement);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost, ScrapedTag } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { TagCategory } from "../BooruTypes";
|
import { TagCategory } from "../BooruTypes";
|
||||||
import { guessContentType, parseResolutionString } from "../Utility";
|
import { guessContentType, parseResolutionString } from "../Utility";
|
||||||
|
|
||||||
export default class AnimePictures implements ScrapeEngine {
|
export default class AnimePictures extends ScrapeEngineBase {
|
||||||
name = "animepictures";
|
name = "animepictures";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "resolution", "tags", "tag_category"];
|
||||||
canImport(url: Location): boolean {
|
notes = ["Rating is assumed to be safe."];
|
||||||
return url.host == "anime-pictures.net";
|
supportedHosts = ["anime-pictures.net"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
post.source = document.location.href;
|
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
const originalImageElements = Array.from(document.getElementsByClassName("download_icon"));
|
const originalImageElements = Array.from(document.getElementsByClassName("download_icon"));
|
||||||
|
|
@ -40,7 +39,7 @@ export default class AnimePictures implements ScrapeEngine {
|
||||||
// Set tags
|
// Set tags
|
||||||
const tagElements = Array.from(document.querySelectorAll<HTMLAnchorElement>("ul.tags > li > a"));
|
const tagElements = Array.from(document.querySelectorAll<HTMLAnchorElement>("ul.tags > li > a"));
|
||||||
for (const tagElement of tagElements) {
|
for (const tagElement of tagElements) {
|
||||||
let tagName = tagElement.innerText;
|
const tagName = tagElement.innerText;
|
||||||
|
|
||||||
let category: TagCategory | undefined;
|
let category: TagCategory | undefined;
|
||||||
for (const className of tagElement.classList) {
|
for (const className of tagElement.classList) {
|
||||||
|
|
@ -58,7 +57,7 @@ export default class AnimePictures implements ScrapeEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName) {
|
if (tagName) {
|
||||||
let tag = new ScrapedTag(tagName, category);
|
const tag = new ScrapedTag(tagName, category);
|
||||||
post.tags.push(tag);
|
post.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapedNote } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { TagCategory } from "../BooruTypes";
|
import { TagCategory } from "../BooruTypes";
|
||||||
import { createNoteFromDanbooruArticle, guessContentType } from "../Utility";
|
import { createNoteFromDanbooruArticle, guessContentType } from "../Utility";
|
||||||
|
|
||||||
export default class Danbooru extends ScrapeEngineBase {
|
export default class Danbooru extends ScrapeEngineBase {
|
||||||
name = "danbooru";
|
name = "danbooru";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "rating", "resolution", "tags", "tag_category", "source", "notes"];
|
||||||
canImport(url: Location): boolean {
|
notes = [];
|
||||||
return url.host == "danbooru.donmai.us" || url.host == "safebooru.donmai.us";
|
supportedHosts = ["danbooru.donmai.us", "safebooru.donmai.us"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
@ -92,7 +91,7 @@ export default class Danbooru extends ScrapeEngineBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName) {
|
if (tagName) {
|
||||||
let tag = new ScrapedTag(tagName, category);
|
const tag = new ScrapedTag(tagName, category);
|
||||||
post.tags.push(tag);
|
post.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost, ContentType } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ContentType, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { guessContentType } from "../Utility";
|
import { guessContentType } from "../Utility";
|
||||||
|
|
||||||
class MediaElement {
|
class MediaElement {
|
||||||
|
|
@ -9,22 +9,21 @@ class MediaElement {
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Fallback implements ScrapeEngine {
|
export default class Fallback extends ScrapeEngineBase {
|
||||||
name = "fallback";
|
name = "fallback";
|
||||||
|
features: ScrapeEngineFeature[] = ["content"];
|
||||||
canImport(location: Location): boolean {
|
notes = ["Tries to find the largest image or video on the current page."];
|
||||||
return false;
|
supportedHosts = [];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
|
|
||||||
// Check if the current page is a link to a video file.
|
// Check if the current page is a link to a video file.
|
||||||
// If it is then use the document.location.href as media source and immediately return.
|
// If it is then use the document.location.href as media source and immediately return.
|
||||||
// This is needed because firefox does not add a <source> to a <video> element
|
// This is needed because firefox does not add a <source> to a <video> element
|
||||||
// when going to a direct link. It looks like chromium-based browsers do not have this issue.
|
// when going to a direct link. It looks like chromium-based browsers do not have this issue.
|
||||||
if (guessContentType(document.location.href) == "video") {
|
if (guessContentType(document.location.href) == "video") {
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
post.contentUrl = document.location.href;
|
post.contentUrl = document.location.href;
|
||||||
post.contentType = "video";
|
post.contentType = "video";
|
||||||
|
|
@ -50,7 +49,7 @@ export default class Fallback implements ScrapeEngine {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (largestMediaElement) {
|
if (largestMediaElement) {
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
post.contentUrl = largestMediaElement.contentUrl;
|
post.contentUrl = largestMediaElement.contentUrl;
|
||||||
post.contentType = largestMediaElement.contentType;
|
post.contentType = largestMediaElement.contentType;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost, ScrapedTag } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { guessContentType, parseResolutionString } from "../Utility";
|
import { guessContentType, parseResolutionString } from "../Utility";
|
||||||
|
|
||||||
export default class FurAffinity implements ScrapeEngine {
|
export default class FurAffinity extends ScrapeEngineBase {
|
||||||
name = "furaffinity";
|
name = "furaffinity";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "rating", "resolution", "tags", "tag_category"];
|
||||||
canImport(url: Location): boolean {
|
notes = [];
|
||||||
return url.host == "www.furaffinity.net";
|
supportedHosts = ["www.furaffinity.net"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
|
const post = new ScrapedPost();
|
||||||
let post = new ScrapedPost();
|
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
@ -53,9 +51,6 @@ export default class FurAffinity implements ScrapeEngine {
|
||||||
.map((x) => (x as HTMLSpanElement).innerText)
|
.map((x) => (x as HTMLSpanElement).innerText)
|
||||||
.map((x) => new ScrapedTag(x));
|
.map((x) => new ScrapedTag(x));
|
||||||
|
|
||||||
// Set source
|
|
||||||
post.source = document.location.href;
|
|
||||||
|
|
||||||
result.tryAddPost(post);
|
result.tryAddPost(post);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { TagCategory } from "../BooruTypes";
|
import { TagCategory } from "../BooruTypes";
|
||||||
import { CategoryMap } from "./Common";
|
import { CategoryMap } from "./Common";
|
||||||
import { createNoteFromDanbooruArticle, guessContentType, parseResolutionString } from "../Utility";
|
import { createNoteFromDanbooruArticle, guessContentType, parseResolutionString } from "../Utility";
|
||||||
|
|
@ -11,6 +11,9 @@ enum Version {
|
||||||
|
|
||||||
export default class Gelbooru extends ScrapeEngineBase {
|
export default class Gelbooru extends ScrapeEngineBase {
|
||||||
name = "gelbooru";
|
name = "gelbooru";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "rating", "resolution", "tags", "tag_category", "source", "notes"];
|
||||||
|
notes = ["Supported features might vary between hosts."];
|
||||||
|
supportedHosts = ["safebooru.org", "gelbooru.com", "rule34.xxx", "tbib.org", "xbooru.com"];
|
||||||
|
|
||||||
private readonly classNameToCategoryMap: CategoryMap = {
|
private readonly classNameToCategoryMap: CategoryMap = {
|
||||||
"tag-type-copyright": "copyright",
|
"tag-type-copyright": "copyright",
|
||||||
|
|
@ -19,18 +22,8 @@ export default class Gelbooru extends ScrapeEngineBase {
|
||||||
"tag-type-metadata": "meta",
|
"tag-type-metadata": "meta",
|
||||||
};
|
};
|
||||||
|
|
||||||
canImport(url: Location): boolean {
|
|
||||||
return (
|
|
||||||
url.host == "safebooru.org" ||
|
|
||||||
url.host == "gelbooru.com" ||
|
|
||||||
url.host == "rule34.xxx" ||
|
|
||||||
url.host == "tbib.org" ||
|
|
||||||
url.host == "xbooru.com"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
let version: Version | null = null;
|
let version: Version | null = null;
|
||||||
switch (document.location.host) {
|
switch (document.location.host) {
|
||||||
case "safebooru.org":
|
case "safebooru.org":
|
||||||
|
|
@ -49,7 +42,7 @@ export default class Gelbooru extends ScrapeEngineBase {
|
||||||
|
|
||||||
this.log("Guessed version: " + version);
|
this.log("Guessed version: " + version);
|
||||||
|
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
@ -154,7 +147,7 @@ export default class Gelbooru extends ScrapeEngineBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tag = new ScrapedTag(tagName, category);
|
const tag = new ScrapedTag(tagName, category);
|
||||||
post.tags.push(tag);
|
post.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost, ScrapedTag } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
|
|
||||||
export default class Inkbunny implements ScrapeEngine {
|
export default class Inkbunny extends ScrapeEngineBase {
|
||||||
name = "inkbunny";
|
name = "inkbunny";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "rating", "resolution", "tags"];
|
||||||
canImport(url: Location): boolean {
|
notes = [];
|
||||||
return url.host == "inkbunny.net";
|
supportedHosts = ["inkbunny.net"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
|
const post = new ScrapedPost();
|
||||||
let post = new ScrapedPost();
|
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
@ -36,7 +34,7 @@ export default class Inkbunny implements ScrapeEngine {
|
||||||
// Set post resolution
|
// Set post resolution
|
||||||
const width = parseInt((<HTMLInputElement>document.getElementById("submission-origwidth"))?.value);
|
const width = parseInt((<HTMLInputElement>document.getElementById("submission-origwidth"))?.value);
|
||||||
const height = parseInt((<HTMLInputElement>document.getElementById("submission-origheight"))?.value);
|
const height = parseInt((<HTMLInputElement>document.getElementById("submission-origheight"))?.value);
|
||||||
if (width != NaN && height != NaN) {
|
if (!isNaN(width) && !isNaN(height)) {
|
||||||
post.resolution = [width, height];
|
post.resolution = [width, height];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +44,7 @@ export default class Inkbunny implements ScrapeEngine {
|
||||||
const ratingEls = spanEls.map((x) => x as HTMLDivElement).filter((x) => x.innerText.indexOf("Rating:") != -1);
|
const ratingEls = spanEls.map((x) => x as HTMLDivElement).filter((x) => x.innerText.indexOf("Rating:") != -1);
|
||||||
|
|
||||||
if (ratingEls.length > 0) {
|
if (ratingEls.length > 0) {
|
||||||
const matches = ratingEls[0].parentElement!.innerText.match(ratingExp);
|
const matches = ratingEls[0].parentElement?.innerText.match(ratingExp);
|
||||||
if (matches && matches.length > 0) {
|
if (matches && matches.length > 0) {
|
||||||
switch (matches[1].toLowerCase()) {
|
switch (matches[1].toLowerCase()) {
|
||||||
case "general":
|
case "general":
|
||||||
|
|
@ -68,7 +66,7 @@ export default class Inkbunny implements ScrapeEngine {
|
||||||
const keywords = keywordsContent.split(",").map((x) => x.trim());
|
const keywords = keywordsContent.split(",").map((x) => x.trim());
|
||||||
|
|
||||||
for (const keyword of keywords) {
|
for (const keyword of keywords) {
|
||||||
let tag = new ScrapedTag(keyword);
|
const tag = new ScrapedTag(keyword);
|
||||||
post.tags.push(tag);
|
post.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapedNote } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { TagCategory } from "../BooruTypes";
|
import { TagCategory } from "../BooruTypes";
|
||||||
import { createNotesFromMoebooruBoxes, guessContentType, parseResolutionString } from "../Utility";
|
import { createNotesFromMoebooruBoxes, guessContentType, parseResolutionString } from "../Utility";
|
||||||
|
|
||||||
export default class Moebooru extends ScrapeEngineBase {
|
export default class Moebooru extends ScrapeEngineBase {
|
||||||
name = "moebooru";
|
name = "moebooru";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "rating", "resolution", "tags", "tag_category", "source", "notes"];
|
||||||
canImport(url: Location): boolean {
|
notes = [];
|
||||||
return url.host == "yande.re" || url.host == "konachan.com";
|
supportedHosts = ["yande.re", "konachan.com"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
@ -64,7 +63,7 @@ export default class Moebooru extends ScrapeEngineBase {
|
||||||
|
|
||||||
for (const el of tagElements) {
|
for (const el of tagElements) {
|
||||||
let tagName: string;
|
let tagName: string;
|
||||||
let tagNameElements = el.getElementsByTagName("a");
|
const tagNameElements = el.getElementsByTagName("a");
|
||||||
|
|
||||||
if (tagNameElements.length > 1) {
|
if (tagNameElements.length > 1) {
|
||||||
// First <a> is "?"
|
// First <a> is "?"
|
||||||
|
|
@ -96,7 +95,7 @@ export default class Moebooru extends ScrapeEngineBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tag = new ScrapedTag(tagName, category);
|
const tag = new ScrapedTag(tagName, category);
|
||||||
post.tags.push(tag);
|
post.tags.push(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { guessContentType } from "../Utility";
|
import { guessContentType } from "../Utility";
|
||||||
|
|
||||||
export function upgradeUrlToOriginalQuality(imgLink: string) {
|
export function upgradeUrlToOriginalQuality(imgLink: string) {
|
||||||
|
|
@ -21,17 +21,15 @@ export function upgradeUrlToOriginalQuality(imgLink: string) {
|
||||||
return imgLink;
|
return imgLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Pixiv implements ScrapeEngine {
|
export default class Pixiv extends ScrapeEngineBase {
|
||||||
name = "pixiv";
|
name = "pixiv";
|
||||||
|
features: ScrapeEngineFeature[] = ["content"];
|
||||||
canImport(url: Location): boolean {
|
notes = [];
|
||||||
return url.host == "pixiv.net" || url.host == "www.pixiv.net";
|
supportedHosts = ["pixiv.net", "www.pixiv.net"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
|
const imgLinks = [] as string[];
|
||||||
let imgLinks = [] as string[];
|
|
||||||
|
|
||||||
// Query for when the user is on the desktop site, is logged in, and has optionally clicked on "See all".
|
// Query for when the user is on the desktop site, is logged in, and has optionally clicked on "See all".
|
||||||
// If the user has not clicked on "See all" then this will only grab the first post.
|
// If the user has not clicked on "See all" then this will only grab the first post.
|
||||||
|
|
@ -49,6 +47,7 @@ export default class Pixiv implements ScrapeEngine {
|
||||||
// This only works on pages with multiple posts. The user can optionally be logged in.
|
// This only works on pages with multiple posts. The user can optionally be logged in.
|
||||||
imgLinks.push(
|
imgLinks.push(
|
||||||
...Array.from(document.querySelectorAll<HTMLImageElement>(".manga-page > a > img[data-big]")).map(
|
...Array.from(document.querySelectorAll<HTMLImageElement>(".manga-page > a > img[data-big]")).map(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
(x) => x.getAttribute("data-big")!
|
(x) => x.getAttribute("data-big")!
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -63,7 +62,7 @@ export default class Pixiv implements ScrapeEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const imgLink of imgLinks) {
|
for (const imgLink of imgLinks) {
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
post.contentUrl = upgradeUrlToOriginalQuality(imgLink);
|
post.contentUrl = upgradeUrlToOriginalQuality(imgLink);
|
||||||
post.contentType = guessContentType(post.contentUrl);
|
post.contentType = guessContentType(post.contentUrl);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { guessContentType } from "../Utility";
|
import { guessContentType } from "../Utility";
|
||||||
|
|
||||||
export default class Reddit implements ScrapeEngine {
|
export default class Reddit extends ScrapeEngineBase {
|
||||||
name = "reddit";
|
name = "reddit";
|
||||||
|
features: ScrapeEngineFeature[] = ["content"];
|
||||||
canImport(url: Location): boolean {
|
notes = [];
|
||||||
return url.host.endsWith("reddit.com");
|
supportedHosts = ["reddit.com", "new.reddit.com", "old.reddit.com"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
// Technically speaking rule34.us also uses gelbooru, albeit a (heavily?) modified one.
|
// Technically speaking rule34.us also uses gelbooru, albeit a (heavily?) modified one.
|
||||||
// Because pretty much no scraper code can be shared with other gelbooru-likes it makes more sense to create a new engine for rule34.us.
|
// Because pretty much no scraper code can be shared with other gelbooru-likes it makes more sense to create a new engine for rule34.us.
|
||||||
|
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost, ScrapedTag } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { TagCategory } from "../BooruTypes";
|
import { TagCategory } from "../BooruTypes";
|
||||||
import { CategoryMap } from "./Common";
|
import { CategoryMap } from "./Common";
|
||||||
import { guessContentType, parseResolutionString } from "../Utility";
|
import { guessContentType, parseResolutionString } from "../Utility";
|
||||||
|
|
||||||
export default class Gelbooru implements ScrapeEngine {
|
export default class Gelbooru extends ScrapeEngineBase {
|
||||||
name = "rule34us";
|
name = "rule34us";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "resolution", "tags", "tag_category"];
|
||||||
|
notes = ["Rating is assumed to be unsafe."];
|
||||||
|
supportedHosts = ["rule34.us"];
|
||||||
|
|
||||||
private readonly classNameToCategoryMap: CategoryMap = {
|
private readonly classNameToCategoryMap: CategoryMap = {
|
||||||
"character-tag": "character",
|
"character-tag": "character",
|
||||||
|
|
@ -16,14 +19,9 @@ export default class Gelbooru implements ScrapeEngine {
|
||||||
"metadata-tag": "meta",
|
"metadata-tag": "meta",
|
||||||
};
|
};
|
||||||
|
|
||||||
canImport(url: Location): boolean {
|
|
||||||
return url.host == "rule34.us";
|
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
|
const post = new ScrapedPost();
|
||||||
let post = new ScrapedPost();
|
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapedNote } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { TagCategory } from "../BooruTypes";
|
import { TagCategory } from "../BooruTypes";
|
||||||
import { createNotesFromMoebooruBoxes, guessContentType } from "../Utility";
|
import { createNotesFromMoebooruBoxes, guessContentType } from "../Utility";
|
||||||
|
|
||||||
export default class SankakuComplex extends ScrapeEngineBase {
|
export default class SankakuComplex extends ScrapeEngineBase {
|
||||||
name = "sankakucomplex";
|
name = "sankakucomplex";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "rating", "tags", "tag_category", "notes"];
|
||||||
canImport(url: Location): boolean {
|
notes = [];
|
||||||
return url.host == "chan.sankakucomplex.com";
|
supportedHosts = ["chan.sankakucomplex.com"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
@ -76,7 +75,7 @@ export default class SankakuComplex extends ScrapeEngineBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName) {
|
if (tagName) {
|
||||||
let tag = new ScrapedTag(tagName, category);
|
const tag = new ScrapedTag(tagName, category);
|
||||||
post.tags.push(tag);
|
post.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost, ScrapedTag } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { guessContentType } from "../Utility";
|
import { guessContentType } from "../Utility";
|
||||||
|
|
||||||
export default class Shimmie2 implements ScrapeEngine {
|
export default class Shimmie2 extends ScrapeEngineBase {
|
||||||
name = "shimmie2";
|
name = "shimmie2";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "tags", "source"];
|
||||||
canImport(url: Location): boolean {
|
notes = ["Rating is assumed to be unsafe."];
|
||||||
return url.host == "rule34.paheal.net" || url.host == "rule34hentai.net";
|
supportedHosts = ["rule34.paheal.net", "rule34hentai.net"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost, ScrapedTag } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { TagCategory } from "../BooruTypes";
|
import { TagCategory } from "../BooruTypes";
|
||||||
import { guessContentType, parseResolutionString } from "../Utility";
|
import { guessContentType, parseResolutionString } from "../Utility";
|
||||||
|
|
||||||
export default class Shuushuu implements ScrapeEngine {
|
export default class Shuushuu extends ScrapeEngineBase {
|
||||||
name = "shuushuu";
|
name = "shuushuu";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "rating", "resolution", "tags", "tag_category"];
|
||||||
canImport(url: Location): boolean {
|
notes = ["Rating is assumed to be safe."];
|
||||||
return url.host == "e-shuushuu.net";
|
supportedHosts = ["e-shuushuu.net"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,29 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
|
|
||||||
export function getOriginalImageUrl(image: HTMLImageElement) {
|
export function getOriginalImageUrl(image: HTMLImageElement) {
|
||||||
// if the link includes a resolution, change it to orig:
|
// if the link includes a resolution, change it to orig:
|
||||||
// https://pbs.twimg.com/media/stuff?format=jpg&name=900x900
|
// https://pbs.twimg.com/media/stuff?format=jpg&name=900x900
|
||||||
// -> https://pbs.twimg.com/media/stuff?format=jpg&name=orig
|
// -> https://pbs.twimg.com/media/stuff?format=jpg&name=orig
|
||||||
let img_url = new URL((image as HTMLImageElement).src);
|
const img_url = new URL((image as HTMLImageElement).src);
|
||||||
if (img_url.searchParams.has("name") && img_url.searchParams.get("name") != "orig") {
|
if (img_url.searchParams.has("name") && img_url.searchParams.get("name") != "orig") {
|
||||||
img_url.searchParams.set("name", "orig");
|
img_url.searchParams.set("name", "orig");
|
||||||
}
|
}
|
||||||
return img_url;
|
return img_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Twitter implements ScrapeEngine {
|
export default class Twitter extends ScrapeEngineBase {
|
||||||
name = "twitter";
|
name = "twitter";
|
||||||
|
features: ScrapeEngineFeature[] = ["content"];
|
||||||
canImport(url: Location): boolean {
|
notes = [];
|
||||||
return url.host == "twitter.com" || url.host == "mobile.twitter.com";
|
supportedHosts = ["twitter.com", "mobile.twitter.com"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
|
|
||||||
// first look for a focused image on desktop:
|
// first look for a focused image on desktop:
|
||||||
const center_img = document.querySelector("div[role=dialog] div[role=dialog] div[aria-label=Image] img");
|
const center_img = document.querySelector("div[role=dialog] div[role=dialog] div[aria-label=Image] img");
|
||||||
if (center_img) {
|
if (center_img) {
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
post.contentUrl = getOriginalImageUrl(center_img as HTMLImageElement).href;
|
post.contentUrl = getOriginalImageUrl(center_img as HTMLImageElement).href;
|
||||||
result.tryAddPost(post);
|
result.tryAddPost(post);
|
||||||
|
|
@ -34,7 +33,7 @@ export default class Twitter implements ScrapeEngine {
|
||||||
const imgs = [...document.querySelectorAll("article a[role='link'] div[data-testid='tweetPhoto'] img")];
|
const imgs = [...document.querySelectorAll("article a[role='link'] div[data-testid='tweetPhoto'] img")];
|
||||||
|
|
||||||
for (const img of imgs) {
|
for (const img of imgs) {
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
post.contentUrl = getOriginalImageUrl(img as HTMLImageElement).href;
|
post.contentUrl = getOriginalImageUrl(img as HTMLImageElement).href;
|
||||||
result.tryAddPost(post);
|
result.tryAddPost(post);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost, ScrapedTag } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { TagCategory } from "../BooruTypes";
|
import { TagCategory } from "../BooruTypes";
|
||||||
import { guessContentType, parseResolutionString } from "../Utility";
|
import { guessContentType, parseResolutionString } from "../Utility";
|
||||||
|
|
||||||
export default class Zerochan implements ScrapeEngine {
|
export default class Zerochan extends ScrapeEngineBase {
|
||||||
name = "zerochan";
|
name = "zerochan";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "resolution", "tags", "tag_category"];
|
||||||
canImport(url: Location): boolean {
|
notes = ["Rating is assumed to be safe."];
|
||||||
return url.host.endsWith("zerochan.net");
|
supportedHosts = ["www.zerochan.net"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
let post = new ScrapedPost();
|
const post = new ScrapedPost();
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
@ -67,12 +66,12 @@ export default class Zerochan implements ScrapeEngine {
|
||||||
category = "artist";
|
category = "artist";
|
||||||
break;
|
break;
|
||||||
case "source":
|
case "source":
|
||||||
// Skip these type of tags, because they are not 'booru-like'
|
// Skip these type of tags, because they are not 'booru-like'.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName) {
|
if (tagName) {
|
||||||
let tag = new ScrapedTag(tagName, category);
|
const tag = new ScrapedTag(tagName, category);
|
||||||
post.tags.push(tag);
|
post.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
import { ScrapeEngine, ScrapeResult, ScrapedPost, ScrapedTag } from "../ScrapeEngine";
|
import { ScrapeEngineBase, ScrapeResult, ScrapedPost, ScrapedTag, ScrapeEngineFeature } from "../ScrapeEngine";
|
||||||
import { TagCategory } from "../BooruTypes";
|
import { TagCategory } from "../BooruTypes";
|
||||||
|
|
||||||
export default class e621 implements ScrapeEngine {
|
export default class e621 extends ScrapeEngineBase {
|
||||||
name = "e621";
|
name = "e621";
|
||||||
|
features: ScrapeEngineFeature[] = ["content", "rating", "resolution", "tags", "tag_category", "source"];
|
||||||
canImport(url: Location): boolean {
|
notes = [];
|
||||||
return url.host == "e621.net" || url.host == "e926.net";
|
supportedHosts = ["e621.net", "e926.net"];
|
||||||
}
|
|
||||||
|
|
||||||
scrapeDocument(document: Document): ScrapeResult {
|
scrapeDocument(document: Document): ScrapeResult {
|
||||||
let result = new ScrapeResult(this.name);
|
const result = new ScrapeResult(this.name);
|
||||||
|
const post = new ScrapedPost();
|
||||||
let post = new ScrapedPost();
|
|
||||||
post.pageUrl = document.location.href;
|
post.pageUrl = document.location.href;
|
||||||
|
|
||||||
// Set image url
|
// Set image url
|
||||||
|
|
@ -40,7 +38,7 @@ export default class e621 implements ScrapeEngine {
|
||||||
// Set resolution
|
// Set resolution
|
||||||
const width = parseInt((<HTMLSpanElement>document.querySelector("span[itemprop='width']"))?.innerText);
|
const width = parseInt((<HTMLSpanElement>document.querySelector("span[itemprop='width']"))?.innerText);
|
||||||
const height = parseInt((<HTMLSpanElement>document.querySelector("span[itemprop='height']"))?.innerText);
|
const height = parseInt((<HTMLSpanElement>document.querySelector("span[itemprop='height']"))?.innerText);
|
||||||
if (width != NaN && height != NaN) {
|
if (!isNaN(width) && !isNaN(height)) {
|
||||||
post.resolution = [width, height];
|
post.resolution = [width, height];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +75,7 @@ export default class e621 implements ScrapeEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName) {
|
if (tagName) {
|
||||||
let tag = new ScrapedTag(tagName, category);
|
const tag = new ScrapedTag(tagName, category);
|
||||||
post.tags.push(tag);
|
post.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export { default as NeoScraper } from "./NeoScraper";
|
export { default as NeoScraper } from "./NeoScraper";
|
||||||
export * from "./ScrapeEngine";
|
export * from "./ScrapeEngine";
|
||||||
|
|
||||||
import * as Engines from "./engines";
|
import * as Engines from "./ScrapeEngine";
|
||||||
export { Engines };
|
export { Engines };
|
||||||
|
|
||||||
import * as BooruTypes from "./BooruTypes";
|
import * as BooruTypes from "./BooruTypes";
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ declare global {
|
||||||
namespace jest {
|
namespace jest {
|
||||||
interface Matchers<R> {
|
interface Matchers<R> {
|
||||||
// The `= undefined` part is incorrect according to VSCode, but it is actually required.
|
// The `= undefined` part is incorrect according to VSCode, but it is actually required.
|
||||||
toHaveTag(tagName: string, tagCategory: TagCategory | undefined = undefined): CustomMatcherResult;
|
toHaveTag(tagName: string, tagCategory: TagCategory | undefined): R;
|
||||||
toHaveNote(noteText: string): CustomMatcherResult;
|
toHaveNote(noteText: string): R;
|
||||||
toHaveResolution(resolution: [number, number]): CustomMatcherResult;
|
toHaveResolution(resolution: [number, number]): R;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ expect.extend({
|
||||||
tagName: string,
|
tagName: string,
|
||||||
tagCategory: TagCategory | undefined = undefined
|
tagCategory: TagCategory | undefined = undefined
|
||||||
): jest.CustomMatcherResult {
|
): jest.CustomMatcherResult {
|
||||||
let pass: boolean = false;
|
let pass = false;
|
||||||
const message = () => (pass ? "" : `Post does not have the '${tagName}' tag, or the category is incorrect.`);
|
const message = () => (pass ? "" : `Post does not have the '${tagName}' tag, or the category is incorrect.`);
|
||||||
|
|
||||||
if (received instanceof ScrapedPost) {
|
if (received instanceof ScrapedPost) {
|
||||||
|
|
@ -35,7 +35,7 @@ expect.extend({
|
||||||
return { pass, message };
|
return { pass, message };
|
||||||
},
|
},
|
||||||
toHaveNote(received: any, noteText: string): jest.CustomMatcherResult {
|
toHaveNote(received: any, noteText: string): jest.CustomMatcherResult {
|
||||||
let pass: boolean = false;
|
let pass = false;
|
||||||
const message = () => (pass ? "" : `Post does not have a note with '${noteText}'.`);
|
const message = () => (pass ? "" : `Post does not have a note with '${noteText}'.`);
|
||||||
|
|
||||||
if (received instanceof ScrapedPost) {
|
if (received instanceof ScrapedPost) {
|
||||||
|
|
@ -44,7 +44,7 @@ expect.extend({
|
||||||
return { pass, message };
|
return { pass, message };
|
||||||
},
|
},
|
||||||
toHaveResolution(received: any, resolution: [number, number]): jest.CustomMatcherResult {
|
toHaveResolution(received: any, resolution: [number, number]): jest.CustomMatcherResult {
|
||||||
let pass: boolean = false;
|
let pass = false;
|
||||||
const message = () => (pass ? "" : `Expected: ${resolution}\nReceived: ${received?.resolution}`);
|
const message = () => (pass ? "" : `Expected: ${resolution}\nReceived: ${received?.resolution}`);
|
||||||
|
|
||||||
if (received instanceof ScrapedPost && received.resolution?.length == 2) {
|
if (received instanceof ScrapedPost && received.resolution?.length == 2) {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2019",
|
"ES2019",
|
||||||
"dom",
|
"DOM",
|
||||||
"dom.iterable"
|
"DOM.Iterable"
|
||||||
],
|
],
|
||||||
"outDir": "./dist/",
|
"outDir": "./dist/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"module": "es2015",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "Node",
|
||||||
"target": "es2015",
|
"target": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue