so much shit + lazy-loaded chapters (ie accessed on first prop access)
parent
fe1b8be519
commit
a764653342
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"deno.enable": true,
|
"deno.enable": true,
|
||||||
"deno.unstable": true
|
"deno.unstable": true
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,46 @@
|
|||||||
import { IAxiodResponse } from "https://deno.land/x/axiod@0.26.2/interfaces.ts";
|
|
||||||
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";
|
|
||||||
import Work from "./Work.ts";
|
import Work from "./Work.ts";
|
||||||
|
import { ID } from "../types.d.ts";
|
||||||
|
import {
|
||||||
|
DOMParser,
|
||||||
|
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/deno-dom-wasm.ts";
|
||||||
|
|
||||||
export default class AO3 {
|
export default class AO3 {
|
||||||
session: typeof axiod
|
session: {
|
||||||
|
get: (path: string) => Promise<Response>;
|
||||||
|
};
|
||||||
|
DOMParser = new DOMParser();
|
||||||
|
|
||||||
constructor(opts?: Record<string, any>) {
|
/**
|
||||||
this.session = axiod.create({
|
* a representation of AO3 in class form
|
||||||
baseURL: opts?.url ?? "https://archiveofourown.org/"
|
*/
|
||||||
})
|
constructor(opts?: {
|
||||||
}
|
url?: string;
|
||||||
|
}) {
|
||||||
|
/*this.session = axiod.create({
|
||||||
|
baseURL: opts?.url ?? "https://archiveofourown.org/",
|
||||||
|
});*/
|
||||||
|
this.session = {
|
||||||
|
get: async (path: string) => {
|
||||||
|
const res = await fetch(
|
||||||
|
opts?.url ?? "https://archiveofourown.org/" + path,
|
||||||
|
);
|
||||||
|
if (res.status > 300) {
|
||||||
|
console.log(res);
|
||||||
|
throw new Error("Failed request, probably rate-limited");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async getWork(id: ID) {
|
/**
|
||||||
const res = await this.session.get(`/works/${id}?view_adult=true&view_full_work=true`)
|
* gets a Work from an ID
|
||||||
return new Work(id, res.data, this.session)
|
* @returns {Promise<Work>} a Work class for the work
|
||||||
}
|
*/
|
||||||
}
|
async getWork(id: ID): Promise<Work> {
|
||||||
|
const res = await this.session.get(
|
||||||
|
`/works/${id}?view_adult=true&view_full_work=true`,
|
||||||
|
);
|
||||||
|
return new Work(id, await res.text(), this.session, new DOMParser());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,116 @@
|
|||||||
|
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";
|
||||||
|
import type {
|
||||||
|
DOMParser,
|
||||||
|
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/dom-parser.ts";
|
||||||
|
import type {
|
||||||
|
Element,
|
||||||
|
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/element.ts";
|
||||||
|
import type {
|
||||||
|
HTMLDocument,
|
||||||
|
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/document.ts";
|
||||||
|
import { ID } from "../types.d.ts";
|
||||||
|
|
||||||
export default class Chapter {
|
export default class Chapter {
|
||||||
constructor(workId: ID, id: ID) {
|
#session: {
|
||||||
}
|
get: (path: string) => Promise<Response>;
|
||||||
}
|
};
|
||||||
|
isInited = false;
|
||||||
|
#document!: HTMLDocument;
|
||||||
|
#DOMParser: DOMParser;
|
||||||
|
#id: ID;
|
||||||
|
#workID: ID;
|
||||||
|
#name!: string;
|
||||||
|
#text!: string;
|
||||||
|
#summary!: string;
|
||||||
|
#startNote!: string;
|
||||||
|
#endNote!: string;
|
||||||
|
id!: Promise<ID>;
|
||||||
|
workID!: Promise<ID>;
|
||||||
|
name!: Promise<string>;
|
||||||
|
text!: Promise<string>;
|
||||||
|
summary!: Promise<string>;
|
||||||
|
startNote!: Promise<string>;
|
||||||
|
endNote!: Promise<string>;
|
||||||
|
|
||||||
|
constructor(workId: ID, id: ID, session: {
|
||||||
|
get: (path: string) => Promise<Response>;
|
||||||
|
}, DOMParser: DOMParser) {
|
||||||
|
this.#session = session;
|
||||||
|
this.#workID = workId;
|
||||||
|
this.#id = id;
|
||||||
|
this.#DOMParser = DOMParser;
|
||||||
|
|
||||||
|
return new Proxy(this, {
|
||||||
|
get: async (target, prop) => {
|
||||||
|
if (!this.isInited) {
|
||||||
|
await target.init();
|
||||||
|
target.isInited = true;
|
||||||
|
}
|
||||||
|
switch (prop) {
|
||||||
|
case "id":
|
||||||
|
return target.#id;
|
||||||
|
case "workID":
|
||||||
|
return target.#workID;
|
||||||
|
case "name":
|
||||||
|
return target.#name;
|
||||||
|
case "text":
|
||||||
|
return target.#text;
|
||||||
|
case "summary":
|
||||||
|
return target.#summary;
|
||||||
|
case "startNote":
|
||||||
|
return target.#startNote;
|
||||||
|
case "endNote":
|
||||||
|
return target.#endNote;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
console.log("initing chapter");
|
||||||
|
const res = await this.#session.get(
|
||||||
|
`/works/${this.#workID}/chapters/${this.#id}?view_adult=true`,
|
||||||
|
);
|
||||||
|
this.#document = this.#DOMParser.parseFromString(
|
||||||
|
await res.text(),
|
||||||
|
"text/html",
|
||||||
|
) as HTMLDocument;
|
||||||
|
this.populateMetadata();
|
||||||
|
this.populateSummary();
|
||||||
|
this.populateNotes();
|
||||||
|
this.populateText();
|
||||||
|
}
|
||||||
|
|
||||||
|
populateMetadata() {
|
||||||
|
this.#name = this.#document.querySelector("h3.title")?.innerText.replace(
|
||||||
|
/Chapter \d+: /,
|
||||||
|
"",
|
||||||
|
).trim() as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
populateSummary() {
|
||||||
|
this.#summary = this.#document.querySelector("#summary > .userstuff")
|
||||||
|
?.innerText.trim() as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
populateNotes() {
|
||||||
|
const notesList = Array.from(
|
||||||
|
this.#document.querySelectorAll(".notes > .userstuff"),
|
||||||
|
).map((n) =>
|
||||||
|
(n as Element).innerHTML
|
||||||
|
);
|
||||||
|
this.#startNote = notesList[0]?.trim()?.replace(/<\/{0,1}p>/g, "\n")?.trim();
|
||||||
|
this.#endNote = notesList[1]?.trim()?.replace(/<\/{0,1}p>/g, "\n")?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
populateText() {
|
||||||
|
/*this.text = this.#document.querySelector("div.userstuff[role='article']")?.innerText.trim().replace(/Chapter Text\s+/, "") as string*/
|
||||||
|
//"div.userstuff[role='article'] > p"
|
||||||
|
Array.from(
|
||||||
|
this.#document.querySelectorAll("div.userstuff[role='article'] > p"),
|
||||||
|
).forEach(
|
||||||
|
(t) => this.#text += (t as Element).innerText + "\n",
|
||||||
|
);
|
||||||
|
this.#text = this.#text.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,41 +1,144 @@
|
|||||||
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts"
|
import Chapter from "./Chapter.ts";
|
||||||
import Chapter from "./Chapter.ts"
|
import type {
|
||||||
import { DOMParser, Element, HTMLDocument } from "https://deno.land/x/deno_dom@v0.1.35-alpha/deno-dom-wasm.ts";
|
DOMParser,
|
||||||
|
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/dom-parser.ts";
|
||||||
|
import type {
|
||||||
|
Element,
|
||||||
|
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/element.ts";
|
||||||
|
import type {
|
||||||
|
HTMLDocument,
|
||||||
|
} from "https://denopkg.dev/gh/Ruthenic/deno-dom@master/src/dom/document.ts";
|
||||||
|
import { ID } from "../types.d.ts";
|
||||||
|
import asyncForEach from "../utils/asyncForeach.ts";
|
||||||
|
|
||||||
export default class Work {
|
export default class Work {
|
||||||
session: typeof axiod
|
#session: {
|
||||||
id: ID
|
get: (path: string) => Promise<Response>;
|
||||||
document: HTMLDocument
|
};
|
||||||
chapters: Chapter[] = []
|
/**
|
||||||
tags: string[] = []
|
* ID of the work
|
||||||
published?: Date
|
*/
|
||||||
updated?: Date
|
id: ID;
|
||||||
|
#document: HTMLDocument;
|
||||||
constructor(id: ID, body: string, session: typeof axiod) {
|
#DOMParser: DOMParser;
|
||||||
this.session = session
|
/**
|
||||||
this.id = id
|
* a list of Chapters in the work
|
||||||
this.document = new DOMParser().parseFromString(body, "text/html") as HTMLDocument
|
*/
|
||||||
this.populateTags()
|
chapters: Chapter[] = [];
|
||||||
this.populateDates()
|
/**
|
||||||
}
|
* a list of tags in the work
|
||||||
|
*/
|
||||||
populateTags() {
|
tags: string[] = [];
|
||||||
Array.from((this.document.querySelector("dd.fandom > ul.commas") as Element).children).map(
|
/**
|
||||||
t => this.tags.push(t.children[0].innerText)
|
* the approximate date the work was published
|
||||||
)
|
*/
|
||||||
Array.from((this.document.querySelector("dd.relationship > ul.commas") as Element).children).map(
|
published?: Date;
|
||||||
t => this.tags.push(t.children[0].innerText)
|
/**
|
||||||
)
|
* the approximate date the work was last updated
|
||||||
Array.from((this.document.querySelector("dd.character > ul.commas") as Element).children).map(
|
*/
|
||||||
t => this.tags.push(t.children[0].innerText)
|
updated?: Date;
|
||||||
)
|
|
||||||
Array.from((this.document.querySelector("dd.freeform > ul.commas") as Element).children).map(
|
/**
|
||||||
t => this.tags.push(t.children[0].innerText)
|
* represents a work on AO3
|
||||||
)
|
* @param id the ID of the work
|
||||||
}
|
* @param body the HTML body of the work (in text)
|
||||||
|
* @param session an axiod session (used for fetching additional details)
|
||||||
populateDates() {
|
*/
|
||||||
this.published = new Date(this.document.querySelector("dd.published")?.innerText as string)
|
constructor(
|
||||||
this.updated = new Date(this.document.querySelector("dd.status")?.innerText as string)
|
id: ID,
|
||||||
}
|
body: string,
|
||||||
}
|
session: {
|
||||||
|
get: (path: string) => Promise<Response>;
|
||||||
|
},
|
||||||
|
DOMParser: DOMParser,
|
||||||
|
) {
|
||||||
|
this.#session = session;
|
||||||
|
this.id = id;
|
||||||
|
this.#document = DOMParser.parseFromString(
|
||||||
|
body,
|
||||||
|
"text/html",
|
||||||
|
) as HTMLDocument;
|
||||||
|
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() {
|
||||||
|
this.published = new Date(
|
||||||
|
this.#document.querySelector("dd.published")?.innerText as string,
|
||||||
|
);
|
||||||
|
this.updated = new Date(
|
||||||
|
this.#document.querySelector("dd.status")?.innerText as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//CW: horrifying jank
|
||||||
|
async populateChapters() {
|
||||||
|
const firstChapterUrl =
|
||||||
|
(this.#document.querySelector("li.chapter > a") as Element).getAttribute(
|
||||||
|
"href",
|
||||||
|
) as string + "?view_adult=true";
|
||||||
|
const res = await this.#session.get(firstChapterUrl);
|
||||||
|
const document = this.#DOMParser.parseFromString(
|
||||||
|
await res.text(),
|
||||||
|
"text/html",
|
||||||
|
) as HTMLDocument;
|
||||||
|
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
Array.from((document.getElementById("selected_id") as Element).children)
|
||||||
|
.sort(
|
||||||
|
(a, b) => {
|
||||||
|
return Number(a.getAttribute("value")) -
|
||||||
|
Number(b.getAttribute("value"));
|
||||||
|
},
|
||||||
|
).forEach((c) => {
|
||||||
|
promises.push((async (c: Element) => {
|
||||||
|
const newChapter = new Chapter(
|
||||||
|
this.id,
|
||||||
|
c.getAttribute("value") as string,
|
||||||
|
this.#session,
|
||||||
|
this.#DOMParser,
|
||||||
|
);
|
||||||
|
await 1; //shut up
|
||||||
|
this.chapters.push(
|
||||||
|
newChapter,
|
||||||
|
);
|
||||||
|
})(c));
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
type ID = BigInt | number | string
|
|
@ -0,0 +1 @@
|
|||||||
|
export type ID = BigInt | number | string;
|
@ -0,0 +1,5 @@
|
|||||||
|
export default async function asyncForEach(array: any[], callback: Function) {
|
||||||
|
for (let index = 0; index < array.length; index++) {
|
||||||
|
await callback(array[index], index, array);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import AO3
|
||||||
|
|
||||||
|
work = AO3.Work(37522864)
|
||||||
|
|
||||||
|
print(f"TAGS: {work.tags}")
|
||||||
|
print(f"PUBLISHED: {work.date_published}")
|
||||||
|
print(f"UPDATED: {work.date_updated}")
|
||||||
|
|
||||||
|
e = len(work.chapters)
|
||||||
|
e = work.id
|
||||||
|
e = work.chapters[0].text
|
||||||
|
e = work.chapters[0].title
|
||||||
|
e = work.chapters[0].text
|
Loading…
Reference in new issue