master
Drake 2 years ago
parent a764653342
commit 60c017360a

@ -1,4 +1,4 @@
{ {
"deno.enable": true, "deno.enable": true,
"deno.unstable": true "deno.unstable": true,
} }

@ -0,0 +1,7 @@
{
"fmt": {
"options": {
"indentWidth": 4
}
}
}

@ -1,46 +1,46 @@
import Work from "./Work.ts"; import Work from "./Work.ts";
import { ID } from "../types.d.ts"; import { ID } from "../types.d.ts";
import { import {
DOMParser, DOMParser,
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/deno-dom-wasm.ts"; } from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/deno-dom-native.ts";
export default class AO3 { export default class AO3 {
session: { session: {
get: (path: string) => Promise<Response>; get: (path: string) => Promise<Response>;
}; };
DOMParser = new DOMParser(); DOMParser = new DOMParser();
/** /**
* a representation of AO3 in class form * a representation of AO3 in class form
*/ */
constructor(opts?: { constructor(opts?: {
url?: string; url?: string;
}) { }) {
/*this.session = axiod.create({ /*this.session = axiod.create({
baseURL: opts?.url ?? "https://archiveofourown.org/", baseURL: opts?.url ?? "https://archiveofourown.org/",
});*/ });*/
this.session = { this.session = {
get: async (path: string) => { get: async (path: string) => {
const res = await fetch( const res = await fetch(
opts?.url ?? "https://archiveofourown.org/" + path, opts?.url ?? "https://archiveofourown.org/" + path,
); );
if (res.status > 300) { if (res.status > 300) {
console.log(res); console.log(res);
throw new Error("Failed request, probably rate-limited"); throw new Error("Failed request, probably rate-limited");
} }
return res; return res;
}, },
}; };
} }
/** /**
* gets a Work from an ID * gets a Work from an ID
* @returns {Promise<Work>} a Work class for the work * @returns {Promise<Work>} a Work class for the work
*/ */
async getWork(id: ID): Promise<Work> { async getWork(id: ID): Promise<Work> {
const res = await this.session.get( const res = await this.session.get(
`/works/${id}?view_adult=true&view_full_work=true`, `/works/${id}?view_adult=true&view_full_work=true`,
); );
return new Work(id, await res.text(), this.session, new DOMParser()); return new Work(id, await res.text(), this.session, new DOMParser());
} }
} }

@ -1,116 +1,119 @@
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts"; import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";
import type { import type {
DOMParser, DOMParser,
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/dom-parser.ts"; } from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/dom-parser.ts";
import type { import type {
Element, Element,
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/element.ts"; } from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/element.ts";
import type { import type {
HTMLDocument, HTMLDocument,
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/document.ts"; } from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/document.ts";
import { ID } from "../types.d.ts"; import { ID } from "../types.d.ts";
export default class Chapter { export default class Chapter {
#session: { #session: {
get: (path: string) => Promise<Response>; get: (path: string) => Promise<Response>;
}; };
isInited = false; isInited = false;
#document!: HTMLDocument; #document!: HTMLDocument;
#DOMParser: DOMParser; #DOMParser: DOMParser;
#id: ID; #id: ID;
#workID: ID; #workID: ID;
#name!: string; #name!: string;
#text!: string; #text!: string;
#summary!: string; #summary!: string;
#startNote!: string; #startNote!: string;
#endNote!: string; #endNote!: string;
id!: Promise<ID>; id!: Promise<ID>;
workID!: Promise<ID>; workID!: Promise<ID>;
name!: Promise<string>; name!: Promise<string>;
text!: Promise<string>; text!: Promise<string>;
summary!: Promise<string>; summary!: Promise<string>;
startNote!: Promise<string>; startNote!: Promise<string>;
endNote!: Promise<string>; endNote!: Promise<string>;
constructor(workId: ID, id: ID, session: { constructor(workId: ID, id: ID, session: {
get: (path: string) => Promise<Response>; get: (path: string) => Promise<Response>;
}, DOMParser: DOMParser) { }, DOMParser: DOMParser) {
this.#session = session; this.#session = session;
this.#workID = workId; this.#workID = workId;
this.#id = id; this.#id = id;
this.#DOMParser = DOMParser; this.#DOMParser = DOMParser;
return new Proxy(this, { return new Proxy(this, {
get: async (target, prop) => { get: async (target, prop) => {
if (!this.isInited) { if (!this.isInited) {
await target.init(); await target.init();
target.isInited = true; target.isInited = true;
} }
switch (prop) { switch (prop) {
case "id": case "id":
return target.#id; return target.#id;
case "workID": case "workID":
return target.#workID; return target.#workID;
case "name": case "name":
return target.#name; return target.#name;
case "text": case "text":
return target.#text; return target.#text;
case "summary": case "summary":
return target.#summary; return target.#summary;
case "startNote": case "startNote":
return target.#startNote; return target.#startNote;
case "endNote": case "endNote":
return target.#endNote; return target.#endNote;
} }
}, },
}); });
} }
async init() { async init() {
console.log("initing chapter"); console.log("initing chapter");
const res = await this.#session.get( const res = await this.#session.get(
`/works/${this.#workID}/chapters/${this.#id}?view_adult=true`, `/works/${this.#workID}/chapters/${this.#id}?view_adult=true`,
); );
this.#document = this.#DOMParser.parseFromString( this.#document = this.#DOMParser.parseFromString(
await res.text(), await res.text(),
"text/html", "text/html",
) as HTMLDocument; ) as HTMLDocument;
this.populateMetadata(); this.populateMetadata();
this.populateSummary(); this.populateSummary();
this.populateNotes(); this.populateNotes();
this.populateText(); this.populateText();
} }
populateMetadata() { populateMetadata() {
this.#name = this.#document.querySelector("h3.title")?.innerText.replace( this.#name = this.#document.querySelector("h3.title")?.innerText
/Chapter \d+: /, .replace(
"", /Chapter \d+: /,
).trim() as string; "",
} ).trim() as string;
}
populateSummary() { populateSummary() {
this.#summary = this.#document.querySelector("#summary > .userstuff") this.#summary = this.#document.querySelector("#summary > .userstuff")
?.innerText.trim() as string; ?.innerText.trim() as string;
} }
populateNotes() { populateNotes() {
const notesList = Array.from( const notesList = Array.from(
this.#document.querySelectorAll(".notes > .userstuff"), this.#document.querySelectorAll(".notes > .userstuff"),
).map((n) => ).map((n) => (n as Element).innerHTML);
(n as Element).innerHTML this.#startNote = notesList[0]?.trim()?.replace(/<\/{0,1}p>/g, "\n")
); ?.trim();
this.#startNote = notesList[0]?.trim()?.replace(/<\/{0,1}p>/g, "\n")?.trim(); this.#endNote = notesList[1]?.trim()?.replace(/<\/{0,1}p>/g, "\n")
this.#endNote = notesList[1]?.trim()?.replace(/<\/{0,1}p>/g, "\n")?.trim(); ?.trim();
} }
populateText() { populateText() {
/*this.text = this.#document.querySelector("div.userstuff[role='article']")?.innerText.trim().replace(/Chapter Text\s+/, "") as string*/ /*this.text = this.#document.querySelector("div.userstuff[role='article']")?.innerText.trim().replace(/Chapter Text\s+/, "") as string*/
//"div.userstuff[role='article'] > p" //"div.userstuff[role='article'] > p"
Array.from( Array.from(
this.#document.querySelectorAll("div.userstuff[role='article'] > p"), this.#document.querySelectorAll(
).forEach( "div.userstuff[role='article'] > p",
(t) => this.#text += (t as Element).innerText + "\n", ),
); ).forEach(
this.#text = this.#text.trim(); (t) => this.#text += (t as Element).innerText + "\n",
} );
this.#text = this.#text.trim();
}
} }

@ -1,144 +1,141 @@
import Chapter from "./Chapter.ts"; import Chapter from "./Chapter.ts";
import type { import type {
DOMParser, DOMParser,
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/dom-parser.ts"; } from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/dom-parser.ts";
import type { import type {
Element, Element,
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/element.ts"; } from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/element.ts";
import type { import type {
HTMLDocument, HTMLDocument,
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/document.ts"; } from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/document.ts";
import { ID } from "../types.d.ts"; import { ID } from "../types.d.ts";
import asyncForEach from "../utils/asyncForeach.ts";
export default class Work { export default class Work {
#session: { #session: {
get: (path: string) => Promise<Response>; get: (path: string) => Promise<Response>;
}; };
/** /**
* ID of the work * ID of the work
*/ */
id: ID; id: ID;
#document: HTMLDocument; #document: HTMLDocument;
#DOMParser: DOMParser; #DOMParser: DOMParser;
/** /**
* a list of Chapters in the work * a list of Chapters in the work
*/ */
chapters: Chapter[] = []; chapters: Chapter[] = [];
/** /**
* a list of tags in the work * a list of tags in the work
*/ */
tags: string[] = []; tags: string[] = [];
/** /**
* the approximate date the work was published * the approximate date the work was published
*/ */
published?: Date; published?: Date;
/** /**
* the approximate date the work was last updated * the approximate date the work was last updated
*/ */
updated?: Date; updated?: Date;
/** /**
* represents a work on AO3 * represents a work on AO3
* @param id the ID of the work * @param id the ID of the work
* @param body the HTML body of the work (in text) * @param body the HTML body of the work (in text)
* @param session an axiod session (used for fetching additional details) * @param session an axiod session (used for fetching additional details)
*/ */
constructor( constructor(
id: ID, id: ID,
body: string, body: string,
session: { session: {
get: (path: string) => Promise<Response>; get: (path: string) => Promise<Response>;
}, },
DOMParser: DOMParser, DOMParser: DOMParser,
) { ) {
this.#session = session; this.#session = session;
this.id = id; this.id = id;
this.#document = DOMParser.parseFromString( this.#document = DOMParser.parseFromString(
body, body,
"text/html", "text/html",
) as HTMLDocument; ) as HTMLDocument;
this.#DOMParser = DOMParser; this.#DOMParser = DOMParser;
} }
//jank incarnate
async init() {
this.populateTags();
this.populateDates();
await this.populateChapters();
}
populateTags() {
Array.from(
(this.#document.querySelector("dd.fandom > ul.commas") as Element)
.children,
).map(
(t) => this.tags.push(t.children[0].innerText),
);
Array.from(
(this.#document.querySelector("dd.relationship > ul.commas") as Element)
.children,
).map(
(t) => this.tags.push(t.children[0].innerText),
);
Array.from(
(this.#document.querySelector("dd.character > ul.commas") as Element)
.children,
).map(
(t) => this.tags.push(t.children[0].innerText),
);
Array.from(
(this.#document.querySelector("dd.freeform > ul.commas") as Element)
.children,
).map(
(t) => this.tags.push(t.children[0].innerText),
);
}
populateDates() { //jank incarnate
this.published = new Date( async init() {
this.#document.querySelector("dd.published")?.innerText as string, this.populateTags();
); this.populateDates();
this.updated = new Date( await this.populateChapters();
this.#document.querySelector("dd.status")?.innerText as string, }
);
}
//CW: horrifying jank populateTags() {
async populateChapters() { Array.from(
const firstChapterUrl = (this.#document.querySelector("dd.fandom > ul.commas") as Element)
(this.#document.querySelector("li.chapter > a") as Element).getAttribute( .children,
"href", ).map(
) as string + "?view_adult=true"; (t) => this.tags.push(t.children[0].innerText),
const res = await this.#session.get(firstChapterUrl); );
const document = this.#DOMParser.parseFromString( Array.from(
await res.text(), (this.#document.querySelector(
"text/html", "dd.relationship > ul.commas",
) as HTMLDocument; ) as Element)
.children,
).map(
(t) => this.tags.push(t.children[0].innerText),
);
Array.from(
(this.#document.querySelector(
"dd.character > ul.commas",
) as Element)
.children,
).map(
(t) => this.tags.push(t.children[0].innerText),
);
Array.from(
(this.#document.querySelector("dd.freeform > ul.commas") as Element)
.children,
).map(
(t) => this.tags.push(t.children[0].innerText),
);
}
const promises: Promise<void>[] = []; populateDates() {
this.published = new Date(
this.#document.querySelector("dd.published")?.innerText as string,
);
this.updated = new Date(
this.#document.querySelector("dd.status")?.innerText as string,
);
}
Array.from((document.getElementById("selected_id") as Element).children) //CW: horrifying jank
.sort( async populateChapters() {
(a, b) => { const firstChapterUrl =
return Number(a.getAttribute("value")) - (this.#document.querySelector("li.chapter > a") as Element)
Number(b.getAttribute("value")); .getAttribute(
}, "href",
).forEach((c) => { ) as string + "?view_adult=true";
promises.push((async (c: Element) => { const res = await this.#session.get(firstChapterUrl);
const newChapter = new Chapter( const document = this.#DOMParser.parseFromString(
this.id, await res.text(),
c.getAttribute("value") as string, "text/html",
this.#session, ) as HTMLDocument;
this.#DOMParser,
);
await 1; //shut up
this.chapters.push(
newChapter,
);
})(c));
});
await Promise.all(promises); Array.from((document.getElementById("selected_id") as Element).children)
} .sort(
(a, b) => {
return Number(a.getAttribute("value")) -
Number(b.getAttribute("value"));
},
).forEach((c) => {
const newChapter = new Chapter(
this.id,
c.getAttribute("value") as string,
this.#session,
this.#DOMParser,
);
this.chapters.push(
newChapter,
);
});
}
} }

@ -1,5 +1,5 @@
export default async function asyncForEach(array: any[], callback: Function) { export default async function asyncForEach(array: any[], callback: Function) {
for (let index = 0; index < array.length; index++) { for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array); await callback(array[index], index, array);
} }
} }

@ -24,6 +24,7 @@ assert(await work.chapters[0].workID == "37522864");
assert(await work.chapters[0].name == "Part I"); assert(await work.chapters[0].name == "Part I");
assert((await work.chapters[0].text).length > 0); assert((await work.chapters[0].text).length > 0);
/*
work = await ao3.getWork("39612636"); work = await ao3.getWork("39612636");
await work.init(); await work.init();
@ -40,4 +41,4 @@ assert(
assert( assert(
(await work.chapters[22].summary) === (await work.chapters[22].summary) ===
"Fizz tries to make the best out of his comeback", "Fizz tries to make the best out of his comeback",
); );.*/

Loading…
Cancel
Save