diff --git a/README.md b/README.md index 7db4cc16..be09fd1e 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Scrapes data from [ao3.org](https://ao3.org) and beyond. Now with Types™. | [**`getWorkWithChapters`**](./src/works/index.ts#L53) | Fetches a work and its chapter list. | `{ workId: string }` - The ID of the work. | [`Promise<{ title: string; authors: Author[] \| Anonymous; workId: string; chapters: Chapter[] }>`](./types/entities.ts) | | [**`getSeries`**](./src/series/index.ts#L17) | Retrieves details for a specific series. | `{ seriesId: string }` - The ID of the series. | [`Promise`](./types/entities.ts) | | [**`getUser`**](./src/users/index.ts#L15) | Fetches profile information for a user. | `{ username: string }` - Username of the user to fetch. | [`Promise`](./types/entities.ts) | +| [**`getUserWorks`**](./src/users/index.ts#L63) | Lists one page of a user's public works. | `{ username: string, pseud?: string, page?: number, sortColumn?, sortDirection? }` | [`Promise`](./types/entities.ts) | | [**`setFetcher`**](./src/fetcher.ts#L5) | Sets a custom fetch function for requests. | `{ fetcher: typeof fetch }` - Custom fetch function. | `void` | #### Why Override Fetch? @@ -56,6 +57,7 @@ Using `setFetcher`, you can override the default `fetch` method used by the libr - **[`WorkSummary`](./types/entities.ts#L20)** / **[`LockedWorkSummary`](./types/entities.ts#L45)**: Summarizes a work, including title, authors, tags, and statistics. - **[`Series`](./types/entities.ts)**: Information on a series, such as title, authors, works, and publication details. - **[`User`](./types/entities.ts)**: Profile information for an AO3 user, including pseudonyms, works, bookmarks, and more. +- **[`UserWorks`](./types/entities.ts)**: One page of a user's public works, with pagination. - **[`Chapter`](./types/entities.ts)**: Details about individual chapters within a work. ## Sample usage diff --git a/src/index.ts b/src/index.ts index 8aafeeaa..e35e85b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,4 @@ export * from "./works"; export * from "./series"; export { getAsShortUrl } from './urls'; export { setFetcher } from "./fetcher"; +export { ArchivePageRequestError, InvalidIDError } from "./utils"; diff --git a/src/page-loaders.ts b/src/page-loaders.ts index 9daa5ee4..f49c3b68 100644 --- a/src/page-loaders.ts +++ b/src/page-loaders.ts @@ -5,6 +5,7 @@ import { getTagWorksFeedAtomUrl, getTagWorksFeedUrl, getUserProfileUrl, + getUserWorksUrl, getWorkIndexUrl, getWorkUrl, } from "./urls"; @@ -12,7 +13,12 @@ import { import { CheerioAPI } from "cheerio"; import { load } from "cheerio/slim"; import { getFetcher } from "./fetcher"; -import { ArchiveId, TagSearchFilters } from "types/entities"; +import { ArchivePageRequestError } from "src/utils"; +import { + ArchiveId, + TagSearchFilters, + UserWorksFilters, +} from "types/entities"; // This is a wrapper around the fetch function that loads the page into a CheerioAPI // instance and returns the type of the page. @@ -25,15 +31,17 @@ const fetchPage = async ({ url: string; skipAdultBanner?: boolean; }) => { - return (await load( - await ( - await getFetcher()(url, { - headers: { - Cookie: skipAdultBanner ? "view_adult=true;" : "", - }, - }) - ).text() - )) as ReturnType; + const response = await getFetcher()(url, { + headers: { + Cookie: skipAdultBanner ? "view_adult=true;" : "", + }, + }); + + if (!response.ok) { + throw new ArchivePageRequestError({ url, status: response.status }); + } + + return (await load(await response.text())) as ReturnType; }; // We create separate interfaces for each page type to make sure that the @@ -118,6 +126,20 @@ export const loadUserProfilePage = async ({ }); }; +// A page listing a user's works, optionally only the ones under one pseud. +// Sample: https://archiveofourown.org/users/astolat/works +// Sample: https://archiveofourown.org/users/astolat/pseuds/astolat/works +export interface UserWorksPage extends CheerioAPI { + kind: "UserWorksPage"; +} +export const loadUserWorksPage = async ( + options: Partial & { username: string; pseud?: string }, +) => { + return await fetchPage({ + url: getUserWorksUrl(options), + }); +}; + export interface ChapterIndexPage extends CheerioAPI { kind: "ChapterIndexPage"; } diff --git a/src/series/getters.ts b/src/series/getters.ts index e77b89de..6c51fce4 100644 --- a/src/series/getters.ts +++ b/src/series/getters.ts @@ -1,32 +1,7 @@ -import type { Author, Series, SeriesWorkSummary } from "types/entities"; -import { type CheerioAPI, load } from "cheerio/slim"; -import type { SeriesPage, WorkPage } from "src/page-loaders"; -import { - getWorkBookmarkCount, - getWorkHits, - getWorkKudosCount, - getWorkLanguage, - getWorkPublishedChapters, - getWorkTotalChapters, - getWorkWordCount, -} from "src/works/work-getters"; -import { getAsShortUrl, getWorkDetailsFromUrl, getWorkUrl } from "src/urls"; -import { parseArchiveId } from "src/utils"; - -const monthMap: { [month: string]: string } = { - Jan: "01", - Feb: "02", - Mar: "03", - Apr: "04", - May: "05", - Jun: "06", - Jul: "07", - Aug: "08", - Sep: "09", - Oct: "10", - Nov: "11", - Dec: "12", -}; +import type { Author, Series, WorkBlurbSummary } from "types/entities"; +import type { SeriesPage } from "src/page-loaders"; +import { getAuthorFromUrl } from "src/urls"; +import { getWorkBlurb } from "src/works/blurb-getters"; export const getSeriesTitle = ($seriesPage: SeriesPage): string => { return $seriesPage("h2.heading").text().trim(); @@ -46,14 +21,7 @@ export const getSeriesAuthors = ( if (authorLinks.length !== 0) { authorLinks.each((i, element) => { - const url = element.attribs.href; - const [, username, pseud] = url.match(/users\/(.+)\/pseuds\/(.+)/)!; - - authors.push({ - username: username, - pseud: decodeURI(pseud), - anonymous: false, - }); + authors.push(getAuthorFromUrl({ url: element.attribs.href })); }); } @@ -117,137 +85,12 @@ export const getSeriesBookmarkCount = ($seriesPage: SeriesPage): number => { export const getSeriesWorks = ( $seriesPage: SeriesPage -): SeriesWorkSummary[] => { - const works: SeriesWorkSummary[] = []; +): WorkBlurbSummary[] => { + const works: WorkBlurbSummary[] = []; $seriesPage("ul.index > li.work").each((index, element) => { - works[index] = getSeriesWork($seriesPage(element).html() as string); + works[index] = getWorkBlurb($seriesPage(element).html() as string); }); return works; }; - -// Helpers for series' works -interface SeriesWork extends CheerioAPI { - kind: "SeriesWork"; -} - -const getSeriesWork = (workHtml: string): SeriesWorkSummary => { - const work = load(workHtml); - const $work = work as SeriesWork, - $$work = work as WorkPage; - - const totalChapters = getWorkTotalChapters($$work); - const publishedChapters = getWorkPublishedChapters($$work); - - const url = $work("a[href*='/works/']").attr("href") as string; - const id = getWorkDetailsFromUrl({ url }).workId; - const workUrl = getWorkUrl({ workId: id }); - const shortUrl = getAsShortUrl({ url: workUrl }); - - return { - id: parseArchiveId(id), - url: workUrl, - shortUrl, - title: getSeriesWorkTitle($work), - updatedAt: getSeriesWorkUpdateDate($work), - - summary: getSeriesWorkSummary($work), - adult: false, - fandoms: getSeriesWorkFandoms($work), - tags: { - characters: getSeriesWorkCharacters($work), - relationships: getSeriesWorkRelationships($work), - additional: getSeriesWorkAdditionalTags($work), - }, - authors: getSeriesWorkAuthors($work), - language: getWorkLanguage($$work), - words: getWorkWordCount($$work), - chapters: { - published: publishedChapters, - total: totalChapters, - }, - complete: totalChapters !== null && totalChapters === publishedChapters, - stats: { - bookmarks: getWorkBookmarkCount($$work), - kudos: getWorkKudosCount($$work), - hits: getWorkHits($$work), - }, - }; -}; - -const getSeriesWorkTitle = ($work: SeriesWork) => { - return $work("h4.heading a[href*='/works/']").text().trim(); -}; - -const getSeriesWorkUpdateDate = ($work: SeriesWork) => { - const [day, month, year] = $work("p.datetime").text().trim().split(" "); - return `${year}-${monthMap[month]}-${day}`; -}; - -const getSeriesWorkSummary = ($work: SeriesWork) => { - const summary = $work("blockquote.summary").html(); - return summary ? summary.trim() : null; -}; - -const getSeriesWorkFandoms = ($work: SeriesWork): string[] => { - const fandoms: string[] = []; - - $work("h5.fandoms a.tag").each(function (i, element) { - fandoms[i] = $work(element).text().trim(); - }); - return fandoms; -}; - -const getSeriesWorkCharacters = ($work: SeriesWork): string[] => { - const characters: string[] = []; - - $work("li.characters a.tag").each(function (i, character) { - characters[i] = $work(character).text().trim(); - }); - return characters; -}; - -const getSeriesWorkRelationships = ($work: SeriesWork): string[] => { - const ships: string[] = []; - - $work("li.relationships a.tag").each(function (i, ship) { - ships[i] = $work(ship).text().trim(); - }); - return ships; -}; - -const getSeriesWorkAdditionalTags = ($work: SeriesWork): string[] => { - const tags: string[] = []; - - $work("li.freeforms a.tag").each(function (i) { - tags[i] = $work(this).text().trim(); - }); - return tags; -}; - -const getSeriesWorkAuthors = ( - $work: SeriesWork -): SeriesWorkSummary["authors"] => { - const authorLinks = $work("h4.heading a[rel='author']"); - const authors: Author[] = []; - - if ($work("h4.heading").text().split("by")[1].trim() === "Anonymous") { - return [{ username: "Anonymous", pseud: "Anonymous", anonymous: true }]; - } - - if (authorLinks.length !== 0) { - authorLinks.each((i, element) => { - const url = element.attribs.href; - const [, username, pseud] = url.match(/users\/(.+)\/pseuds\/(.+)/)!; - - authors.push({ - username: username, - pseud: decodeURI(pseud), - anonymous: false, - }); - }); - } - - return authors; -}; diff --git a/src/tags/search-getters.ts b/src/tags/search-getters.ts index 4f31281b..2f1d8c99 100644 --- a/src/tags/search-getters.ts +++ b/src/tags/search-getters.ts @@ -1,4 +1,4 @@ -import { TagSearchPage } from "src/page-loaders"; +import { TagSearchPage, UserWorksPage } from "src/page-loaders"; import { TagSearchResultSummary } from "types/entities"; const parseIntOrThrow = (text: string) => { @@ -17,7 +17,7 @@ export const getTotalResults = (page: TagSearchPage) => { return totalResultsMatch ? parseIntOrThrow(totalResultsMatch[1]) : 0; }; -export const getPagesCount = (page: TagSearchPage) => { +export const getPagesCount = (page: TagSearchPage | UserWorksPage) => { const lastPageMatch = page(".pagination.actions li:not(.next, .previous)") .last() .text(); diff --git a/src/urls.ts b/src/urls.ts index b187e680..c584b809 100644 --- a/src/urls.ts +++ b/src/urls.ts @@ -4,7 +4,12 @@ import { isValidArchiveIdOrNullish, parseArchiveId, } from "./utils"; -import { TagSearchFilters, WorkSummary } from "types/entities"; +import { + Author, + TagSearchFilters, + UserWorksFilters, + WorkSummary, +} from "types/entities"; declare global { var archiveBaseUrl: string; @@ -119,6 +124,51 @@ export const getDownloadUrls = ({ export const getUserProfileUrl = ({ username }: { username: string }) => new URL(`/users/${encodeURI(username)}/profile`, getArchiveBaseUrl()).href; +const USER_WORKS_SORT_COLUMNS: Record = { + authors: "authors_to_sort_on", + title: "title_to_sort_on", + created_at: "created_at", + updated_at: "revised_at", + word_count: "word_count", + hits: "hits", + kudos_count: "kudos_count", + comments_count: "comments_count", + bookmarks_count: "bookmarks_count", +}; + +export const getUserWorksUrl = ({ + username, + pseud, + page, + sortColumn, + sortDirection, +}: Partial & { username: string; pseud?: string }) => { + // Pseud names are only unique within an account, so AO3 nests them under + // each user. + const userPath = `/users/${encodeURI(username)}`; + const url = new URL( + pseud === undefined + ? `${userPath}/works` + : `${userPath}/pseuds/${encodeURI(pseud)}/works`, + getArchiveBaseUrl(), + ); + + if (page !== undefined) { + url.searchParams.set("page", String(page)); + } + if (sortColumn !== undefined) { + url.searchParams.set( + "work_search[sort_column]", + USER_WORKS_SORT_COLUMNS[sortColumn], + ); + } + if (sortDirection) { + url.searchParams.set("work_search[sort_direction]", sortDirection); + } + + return url.href; +}; + const TOKEN_REPLACEMENTS_MAP = { "/": "*s*", "&": "*a*", @@ -166,6 +216,45 @@ export const getTagWorksFeedUrl = (tagName: string) => export const getTagWorksFeedAtomUrl = (tagId: string) => new URL(`tags/${tagId}/feed.atom`, getArchiveBaseUrl()).href; +/** + * Reads the username (and pseud, if any) from a user URL. + * + * The username is returned exactly as it appears in the URL. AO3 matches + * usernames in any letter case (e.g. /users/franzeska loads the account + * "Franzeska"), so usernames from user-given links may not match the ones + * in AO3's own author links. Make sure to use .toLowerCase() when comparing + * them. + */ +export const getUserDetailsFromUrl = ({ + url, +}: { + url: string; +}): { + username: string; + pseud?: string; +} => { + const match = url.match(/\/users\/([^/?#]+)(?:\/pseuds\/([^/?#]+))?/); + if (!match) { + throw new Error(`Invalid user URL: ${url}`); + } + + // Without a pseud the URL points to the whole account, not to the + // user's default pseud (which can have a different name). + const [, username, pseud] = match; + return pseud === undefined + ? { username } + : { username, pseud: decodeURI(pseud) }; +}; + +export const getAuthorFromUrl = ({ url }: { url: string }): Author => { + const { username, pseud } = getUserDetailsFromUrl({ url }); + if (pseud === undefined) { + throw new Error(`Unexpected author URL: ${url}`); + } + + return { username, pseud, anonymous: false }; +}; + export const getWorkDetailsFromUrl = ({ url, }: { @@ -211,7 +300,7 @@ const getSearchParamsFromTagFilters = ( type: searchFilters.type && searchFilters.type !== "any" ? searchFilters.type.charAt(0).toUpperCase() + - searchFilters.type.slice(1).toLowerCase() + searchFilters.type.slice(1).toLowerCase() : "", wrangling_status: searchFilters.wranglingStatus diff --git a/src/users/getters.ts b/src/users/getters.ts index 80c0b250..3ce022cc 100644 --- a/src/users/getters.ts +++ b/src/users/getters.ts @@ -1,5 +1,6 @@ import type { UserProfile } from "src/page-loaders"; import { parseArchiveId } from "src/utils"; +import { getAuthorFromUrl } from "src/urls"; // import { getUserProfileUrl } from "../urls"; //Dates are ten characters long in the following format: @@ -23,10 +24,7 @@ export const getUserProfilePseuds = ($userProfile: UserProfile) => { if (pseuds.length !== 0) { pseuds.each((i, element) => { - const url = element.attribs.href; - const [, username, pseud] = url.match(/users\/(.+)\/pseuds\/(.+)/)!; - - pseudsArray.push(decodeURI(pseud)); + pseudsArray.push(getAuthorFromUrl({ url: element.attribs.href }).pseud); }); } return pseudsArray.join(PSEUD_SUFFIX); diff --git a/src/users/index.ts b/src/users/index.ts index d1ac4e7d..e34354d8 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -15,9 +15,16 @@ import { getUserProfileWorks, } from "./getters"; -import type { User } from "types/entities"; +import type { + User, + UserWorks, + UserWorksFilters, +} from "types/entities"; import { getAsShortUrl, getUserProfileUrl } from "src/urls"; -import { loadUserProfilePage } from "src/page-loaders"; +import { loadUserProfilePage, loadUserWorksPage } from "src/page-loaders"; +import { getUserWorksBlurbs, getUserWorksTotalResults } from "./works-getters"; +import { getWorkBlurb } from "src/works/blurb-getters"; +import { getPagesCount } from "src/tags/search-getters"; export const getUser = async ({ username, @@ -48,3 +55,48 @@ export const getUser = async ({ bioHtml: getUserProfileBio(profilePage), }; }; + +export const getUserWorks = async ({ + username, + pseud, + ...userWorksFilters +}: Partial & { + username: string; + pseud?: string; +}): Promise => { + + const sortColumn = userWorksFilters.sortColumn ?? "updated_at"; + const defaultSortDirection = sortColumn === "authors" || sortColumn === "title" + ? "asc" + : "desc"; + const normalizedFilters: UserWorksFilters = { + sortColumn, + sortDirection: + userWorksFilters.sortDirection ?? defaultSortDirection, + page: userWorksFilters.page ?? 1, + }; + + const worksPage = await loadUserWorksPage({ + username, + pseud, + ...normalizedFilters, + // We only send the direction when it was requested, so the URL matches + // the one AO3 itself links to. + sortDirection: userWorksFilters.sortDirection, + }); + const blurbs = getUserWorksBlurbs(worksPage); + + return { + // We return the filters as is because they are already normalized. + filters: normalizedFilters, + totalResults: getUserWorksTotalResults(worksPage), + pages: { + total: + blurbs.length > 0 + ? Math.max(1, getPagesCount(worksPage)) + : 0, + current: normalizedFilters.page, + }, + works: blurbs.map(getWorkBlurb), + }; +}; diff --git a/src/users/works-getters.ts b/src/users/works-getters.ts new file mode 100644 index 00000000..0568ce85 --- /dev/null +++ b/src/users/works-getters.ts @@ -0,0 +1,17 @@ +import type { UserWorksPage } from "src/page-loaders"; + +export const getUserWorksBlurbs = ($worksPage: UserWorksPage) => { + if ($worksPage("#main.works-index").length === 0) { + throw new Error("Works page is missing the expected public index"); + } + + return $worksPage("ol.work.index > li.work.blurb") + .map((_index, element) => $worksPage(element).html() ?? "") + .get(); +}; + +export const getUserWorksTotalResults = ($worksPage: UserWorksPage) => { + const heading = $worksPage("#main.works-index h2.heading").first().text(); + const totalMatch = heading.match(/([\d,]+)\s+Works?\b/i); + return totalMatch ? Number(totalMatch[1].replaceAll(",", "")) : 0; +}; diff --git a/src/utils.ts b/src/utils.ts index f3390ecc..d0db6916 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,3 +21,41 @@ export class InvalidIDError extends Error { this.message = `${id} is not a valid ${type} id`; } } + +export class ArchivePageRequestError extends Error { + message: string; + status: number; + url: string; + + constructor({ url, status }: { url: string; status: number }) { + super(); + this.message = `Archive request failed with status ${status}: ${url}`; + this.status = status; + this.url = url; + } +} + +const MONTHS: Record = { + Jan: "01", + Feb: "02", + Mar: "03", + Apr: "04", + May: "05", + Jun: "06", + Jul: "07", + Aug: "08", + Sep: "09", + Oct: "10", + Nov: "11", + Dec: "12", +}; + +// AO3 blurbs (works, series, bookmarks) show dates like "01 Jan 2026"; +// returns "2026-01-01". +export const parseBlurbDate = (text: string) => { + const [day, month, year] = text.trim().split(/\s+/); + if (!day || !MONTHS[month] || !year) { + throw new Error(`Unexpected work blurb date: ${text}`); + } + return `${year}-${MONTHS[month]}-${day.padStart(2, "0")}`; +}; diff --git a/src/works/blurb-getters.ts b/src/works/blurb-getters.ts new file mode 100644 index 00000000..3f97143e --- /dev/null +++ b/src/works/blurb-getters.ts @@ -0,0 +1,106 @@ +import type { Author, WorkBlurbSummary } from "types/entities"; +import { type CheerioAPI, load } from "cheerio/slim"; +import { + getWorkBookmarkCount, + getWorkHits, + getWorkKudosCount, + getWorkLanguage, + getWorkPublishedChapters, + getWorkTotalChapters, + getWorkWordCount, +} from "src/works/work-getters"; +import { + getAsShortUrl, + getAuthorFromUrl, + getWorkDetailsFromUrl, + getWorkUrl, +} from "src/urls"; +import { parseArchiveId, parseBlurbDate } from "src/utils"; + +// AO3 renders works with the same blurb markup in every listing (series pages, +// user works pages...), so they all share this parser. +export interface WorkBlurb extends CheerioAPI { + kind: "WorkBlurb"; +} + +export const getWorkBlurb = (blurbHtml: string): WorkBlurbSummary => { + const $work = load(blurbHtml) as WorkBlurb; + + const relativeUrl = $work("h4.heading a[href*='/works/']") + .first() + .attr("href"); + const title = $work("h4.heading a[href*='/works/']").first().text().trim(); + + if (!relativeUrl || !title) { + throw new Error("Work blurb is missing its title or URL"); + } + + const id = getWorkDetailsFromUrl({ url: relativeUrl }).workId; + const url = getWorkUrl({ workId: id }); + const totalChapters = getWorkTotalChapters($work); + const publishedChapters = getWorkPublishedChapters($work); + + return { + id: parseArchiveId(id), + url, + shortUrl: getAsShortUrl({ url }), + title, + updatedAt: parseBlurbDate($work("p.datetime").text()), + + summary: getWorkBlurbSummary($work), + adult: false, + fandoms: getWorkBlurbTags($work, "h5.fandoms a.tag"), + tags: { + characters: getWorkBlurbTags($work, "li.characters a.tag"), + relationships: getWorkBlurbTags($work, "li.relationships a.tag"), + additional: getWorkBlurbTags($work, "li.freeforms a.tag"), + }, + authors: getWorkBlurbAuthors($work), + language: getWorkLanguage($work), + words: getWorkWordCount($work), + chapters: { + published: publishedChapters, + total: totalChapters, + }, + complete: totalChapters !== null && totalChapters === publishedChapters, + stats: { + bookmarks: getWorkBookmarkCount($work), + kudos: getWorkKudosCount($work), + hits: getWorkHits($work), + }, + }; +}; + +const getWorkBlurbSummary = ($work: WorkBlurb) => { + const summary = $work("blockquote.summary").html(); + return summary ? summary.trim() : null; +}; + +const getWorkBlurbTags = ($work: WorkBlurb, selector: string) => { + return $work(selector) + .map((_index, element) => $work(element).text().trim()) + .get(); +}; + +const getWorkBlurbAuthors = ($work: WorkBlurb) => { + const authorLinks = $work("h4.heading a[rel='author']"); + + // Anonymous works list "Anonymous" as plain text instead of an author link. + if ( + authorLinks.length === 0 && + /\bby\s+Anonymous\s*$/.test($work("h4.heading").text()) + ) { + return [{ username: "Anonymous", pseud: "Anonymous", anonymous: true }]; + } + + const authors: Author[] = []; + authorLinks.each((_index, element) => { + authors.push(getAuthorFromUrl({ url: element.attribs.href })); + }); + + if (authors.length === 0) { + throw new Error("Work blurb is missing its author"); + } + + return authors; +}; diff --git a/src/works/chapter-getters.ts b/src/works/chapter-getters.ts index 9d5ae303..44cfad66 100644 --- a/src/works/chapter-getters.ts +++ b/src/works/chapter-getters.ts @@ -1,5 +1,10 @@ import type { Author, Chapter } from "types/entities"; -import { getAsShortUrl, getWorkDetailsFromUrl, getWorkUrl } from "src/urls"; +import { + getAsShortUrl, + getAuthorFromUrl, + getWorkDetailsFromUrl, + getWorkUrl, +} from "src/urls"; import { ChapterIndexPage } from "src/page-loaders"; import { parseArchiveId } from "src/utils"; @@ -50,14 +55,7 @@ export const getWorkAuthors = ( if (authorNode.length !== 0) { authorNode.each((i, element) => { - const url = element.attribs.href; - const [, username, pseud] = url.match(/users\/(.+)\/pseuds\/(.+)/)!; - - authors.push({ - username: username, - pseud: decodeURI(pseud), - anonymous: false, - }); + authors.push(getAuthorFromUrl({ url: element.attribs.href })); }); } return authors; diff --git a/src/works/work-getters.ts b/src/works/work-getters.ts index 1d6ff132..c7d5581b 100644 --- a/src/works/work-getters.ts +++ b/src/works/work-getters.ts @@ -7,8 +7,10 @@ import { WorkWarnings, } from "types/entities"; import { isValidArchiveId, parseArchiveId } from "src/utils"; +import { getAuthorFromUrl } from "src/urls"; import { WorkPage } from "src/page-loaders"; +import type { WorkBlurb } from "src/works/blurb-getters"; export const getWorkAuthors = ($workPage: WorkPage): Author[] => { const authorLinks = $workPage("h3.byline a[rel='author']"); @@ -20,14 +22,7 @@ export const getWorkAuthors = ($workPage: WorkPage): Author[] => { if (authorLinks.length !== 0) { authorLinks.each((i, element) => { - const url = element.attribs.href; - const [, username, pseud] = url.match(/users\/(.+)\/pseuds\/(.+)/)!; - - authors.push({ - username: username, - pseud: decodeURI(pseud), - anonymous: false, - }); + authors.push(getAuthorFromUrl({ url: element.attribs.href })); }); } @@ -38,11 +33,11 @@ export const getWorkTitle = ($workPage: WorkPage): string => { return $workPage("h2.title").text().trim(); }; -export const getWorkWordCount = ($workPage: WorkPage): number => { +export const getWorkWordCount = ($workPage: WorkPage | WorkBlurb): number => { return parseInt($workPage("dd.words").text().replaceAll(",", "").trim()); }; -export const getWorkLanguage = ($workPage: WorkPage): string => { +export const getWorkLanguage = ($workPage: WorkPage | WorkBlurb): string => { return $workPage("dd.language").text().trim(); }; @@ -132,11 +127,15 @@ export const getWorkPublishDate = ($workPage: WorkPage): string => { return $workPage("dd.published").text().trim(); }; -export const getWorkPublishedChapters = ($workPage: WorkPage): number => { +export const getWorkPublishedChapters = ( + $workPage: WorkPage | WorkBlurb +): number => { return parseInt($workPage("dd.chapters").text().trim().split("/")[0]); }; -export const getWorkTotalChapters = ($workPage: WorkPage): number | null => { +export const getWorkTotalChapters = ( + $workPage: WorkPage | WorkBlurb +): number | null => { const totalChapters = $workPage("dd.chapters").text().trim().split("/")[1]; return totalChapters === "?" ? null : parseInt(totalChapters); @@ -178,13 +177,13 @@ export const getWorkCommentCount = ($workPage: WorkPage): number => { return comments ? parseInt(comments) : 0; }; -export const getWorkKudosCount = ($workPage: WorkPage) => { +export const getWorkKudosCount = ($workPage: WorkPage | WorkBlurb) => { const kudos = $workPage("dd.kudos").text().replaceAll(",", "").trim(); return kudos ? parseInt(kudos) : 0; }; -export const getWorkBookmarkCount = ($workPage: WorkPage) => { +export const getWorkBookmarkCount = ($workPage: WorkPage | WorkBlurb) => { const bookmarks = $workPage("dd.bookmarks a") .text() .replaceAll(",", "") @@ -193,7 +192,7 @@ export const getWorkBookmarkCount = ($workPage: WorkPage) => { return bookmarks ? parseInt(bookmarks) : 0; }; -export const getWorkHits = ($workPage: WorkPage) => { +export const getWorkHits = ($workPage: WorkPage | WorkBlurb) => { return parseInt($workPage("dd.hits").text().replaceAll(",", "").trim()); }; diff --git a/tests/mocks/data/ao3/users/astolat/pseuds/astolat/works.html b/tests/mocks/data/ao3/users/astolat/pseuds/astolat/works.html new file mode 100644 index 00000000..faa0a28b --- /dev/null +++ b/tests/mocks/data/ao3/users/astolat/pseuds/astolat/works.html @@ -0,0 +1,2947 @@ + + + + + + + + + + + + + + + + astolat - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 516 Works by astolat +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + +
    + +

    + Research + by + + + + + + + + +

    + +
    + Fandoms: + Harry Potter - J. K. Rowling +   +
    + + + +

    21 Dec 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Ever since she’d been small, Hermione’s reward for good marks had been a rampage through a bookshop, so the day she tore open her O.W.L.s results and triumphantly brandished the eleven Outstandings, her beaming mother said, “We’ll go to Foyles tomorrow,” without any discussion required.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    36,337
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    613
    + +
    Kudos:
    +
    5,159
    + +
    Bookmarks:
    +
    1,625
    + +
    Hits:
    +
    49,343
    + +
    + + +
  2. +
  3. + +
    + +

    + Inheritance + by + + + + + + + + +

    + +
    + Fandoms: + Lucifer (TV) +   +
    + + + +

    21 Dec 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    He didn’t tell her, I’ll die if you leave me, even though it was quite literally true.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    3,921
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    65
    + +
    Kudos:
    +
    925
    + +
    Bookmarks:
    +
    81
    + +
    Hits:
    +
    5,162
    + +
    + + +
  4. +
  5. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Legolas looked at the Dwarf to dislike, when first the Fellowship left Rivendell, and was satisfied with everything he saw. To the memory-shadow of Tauriel that lived yet in his mind, he listed the faults one after another. Did you truly love such a one? he asked her silently, disdainful.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,825
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    60
    + +
    Kudos:
    +
    1,313
    + +
    Bookmarks:
    +
    111
    + +
    Hits:
    +
    6,753
    + +
    + + +
  6. +
  7. + +
    + +

    + Hallelujah + by + + + + + + + + +

    + +
    + Fandoms: + Good Omens (TV) +   +
    + + + +

    18 Dec 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    This most wonderful and unexpected of all days, the first day after the end of the world.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    848
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    62
    + +
    Kudos:
    +
    864
    + +
    Bookmarks:
    +
    54
    + +
    Hits:
    +
    4,206
    + +
    + + +
  8. +
  9. + +
    + +

    + Tourney Field + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    17 Dec 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    “A friend from Derry tells me that Lord Stark is entering the tourney at the Crossings,” Tyrion said.

    +

    “He’s what?” Jaime said incredulously. “His first tourney at forty?”

    +

    “Not that Lord Stark,” Tyrion said. “The young one. Robb, I think his name is.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,688
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    235
    + +
    Kudos:
    +
    3,031
    + +
    Bookmarks:
    +
    325
    + +
    Hits:
    +
    20,710
    + +
    + + +
  10. +
  11. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    He was dying down here a little piece at a time, and if it took too long to get him out, there wouldn’t be a lot left of the guy he remembered in the mirror.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    3,479
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    68
    + +
    Kudos:
    +
    942
    + +
    Bookmarks:
    +
    67
    + +
    Hits:
    +
    5,875
    + +
    + + +
  12. +
  13. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    “Where did you come from?” Dorian demanded.

    +

    “He was so loud,” Cole said, blinking at him with all that disingenuous blue. “She’s taken, hurt, hurting, and I have to bear it: holding a debt so large my life and soul can’t lift it, so I have to give my heart and wholeness, too. But they’re going to cut her off, let the magic all run out and leave the hollow shell to haunt me, and I have to bear that too, but it’s too high a price; I have to go to her.” He shrugged. “So I came to help him.”

    +

    “Let’s keep going,” Bull said.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,233
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    32
    + +
    Kudos:
    +
    400
    + +
    Bookmarks:
    +
    59
    + +
    Hits:
    +
    3,030
    + +
    + + +
  14. +
  15. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Bucky didn’t open his eyes: there was no imminent threat. The device on the other side of the bed was just chiming a wake-up call at Stark, who started complaining at Jarvis; shortly after that, Potts started complaining at Stark. “Oh my God,” she groaned. “Tony, I don’t believe you. How could you talk me into this? You know what happens when I drink too much champagne!”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    3,786
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    67
    + +
    Kudos:
    +
    1,004
    + +
    Bookmarks:
    +
    63
    + +
    Hits:
    +
    5,890
    + +
    + + +
  16. +
  17. + +
    + +

    + Common Knowledge + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    11 Jan 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    It had never occurred to Jaime once, in the three solid months he’d spent in the pen, that Robb Stark could make his imprisonment worse by giving him a bath, a warm bed, and some decent food to eat.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,721
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    290
    + +
    Kudos:
    +
    5,141
    + +
    Bookmarks:
    +
    762
    + +
    Hits:
    +
    53,291
    + +
    + + +
  18. +
  19. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    There is a land of the living and a land of the dead and the bridge is love, the only survival.
    —Thornton Wilder

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    176,760
    +
    Chapters:
    +
    23/23
    + + + +
    Comments:
    +
    3,627
    + +
    Kudos:
    +
    5,883
    + +
    Bookmarks:
    +
    1,611
    + +
    Hits:
    +
    184,898
    + +
    + + +
  20. +
  21. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    “The girl asks for more deaths than she is owed. The Many-Faced God may grant it. But for this, there will be a price. And a man cannot say what the price will be. A girl must pay. A man must pay. A girl’s brother must pay, if he agrees.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    12,034
    +
    Chapters:
    +
    2/2
    + + + +
    Comments:
    +
    309
    + +
    Kudos:
    +
    4,737
    + +
    Bookmarks:
    +
    851
    + +
    Hits:
    +
    75,720
    + +
    + + +
  22. +
  23. + +
    + +

    + A Man of Honor + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    14 Jan 2023

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    “Yes, Lord Stark has come to town to help his mother bring out his eldest sister,” Tyrion said, pouring Jaime a glass of wine. “I understand he also plans to take his seat in the House of the Law. His uncle Edmure has been gathering time for him to give his maiden speech.”

    “Yes, yes,” Jaime said irritably. “That’s not what matters and you know it. Well?”

    Tyrion sighed. “He's nineteen, well-favored, and as far as I know, neither demented nor stupid. And not a hint of a betrothal. Sorry.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    36,460
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    411
    + +
    Kudos:
    +
    5,318
    + +
    Bookmarks:
    +
    1,320
    + +
    Hits:
    +
    69,894
    + +
    + + +
  24. +
  25. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    The deep satisfaction of having made the right choice; of having found a clear-flowing wellspring of true honor to protect.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    61,498
    +
    Chapters:
    +
    9/9
    + + + +
    Comments:
    +
    1,384
    + +
    Kudos:
    +
    5,686
    + +
    Bookmarks:
    +
    1,255
    + +
    Hits:
    +
    111,750
    + +
    + + +
  26. +
  27. + +
    + +

    + Winter's Crown + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    13 Nov 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    “When the Night’s King rides,” the giant said, each word slow as cold honey pouring, “the King in the North must answer. The King in the North…whose name is Stark.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    76,073
    +
    Chapters:
    +
    13/13
    + + + +
    Comments:
    +
    2,313
    + +
    Kudos:
    +
    6,389
    + +
    Bookmarks:
    +
    1,598
    + +
    Hits:
    +
    123,622
    + +
    + + +
  28. +
  29. + +
    + +

    + Royal Flush + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    08 Jun 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Robb Stark had swept his entire hand of cards off the table, and Tyrion couldn’t see how to make a single play at all.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    85,124
    +
    Chapters:
    +
    16/16
    + + + +
    Comments:
    +
    3,284
    + +
    Kudos:
    +
    7,637
    + +
    Bookmarks:
    +
    2,039
    + +
    Hits:
    +
    200,842
    + +
    + + +
  30. +
  31. + +
    + +

    + Lifeline + by + + + + + + + + +

    + +
    + Fandoms: + Fast & Furious (Movies) +   +
    + + + +

    19 May 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Vampires dominate by biting, alpha wolves dominate by fucking. What the hell’s going on between the two of you, I don’t even have a clue."

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    37,951
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    344
    + +
    Kudos:
    +
    4,662
    + +
    Bookmarks:
    +
    1,295
    + +
    Hits:
    +
    58,426
    + +
    + + +
  32. +
  33. + +
    + +

    + Heal Thyself + by + + + + + + + + +

    + +
    + Fandoms: + Harry Potter - J. K. Rowling +   +
    + + + +

    07 Apr 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Are you going for the course?" Lovegood asked. "You have the NEWTs.”

    “What course?” Draco said, then, “No, don’t be ridiculous,” when he realized she meant the notice pinned up on the board he’d been staring at: Applicants To The Introductory Mediwizard Course For The Coming Term Shall Present Themselves In The Chief Mediwizard’s Office By August 24th.

    “Oh, I thought you might,” she said. “Well, goodbye.” And off she wandered again in her addled way.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    46,905
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    1,711
    + +
    Kudos:
    +
    26,901
    + +
    Bookmarks:
    +
    9,934
    + +
    Hits:
    +
    425,942
    + +
    + + +
  34. +
  35. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Jaime didn’t like the Starks at all. He didn’t like their earnestness and honesty and prudishness, he didn’t like their cold heavy-walled castle, he didn’t like their frozen block of a realm, and he especially didn’t like Robb Stark, who had saved his son’s life and been kind to him and got him to sleep, and what business of his was it to have done any of those things?

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    26,976
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    499
    + +
    Kudos:
    +
    7,552
    + +
    Bookmarks:
    +
    2,096
    + +
    Hits:
    +
    94,859
    + +
    + + +
  36. +
  37. + +
    + +

    + Paying Debts + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    23 Feb 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Tywin gave her a stern look. “Watch your tongue.”

    “Or what, you’ll cut my head off, like your grandson did to my father?” Arya spat at him, a cub trying to bite with milk teeth.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    7,667
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    510
    + +
    Kudos:
    +
    8,349
    + +
    Bookmarks:
    +
    1,703
    + +
    Hits:
    +
    85,494
    + +
    + + +
  38. +
  39. + +
    + +

    + The Next Time + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    26 Jan 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Jaime looked back at Robb with rage blazing through him and said, low and utterly flat, “The next time I see you, Stark, I’ll kill you.”

    “The next time I see you, Ser Jaime,” Robb said, “if you ask nicely, I’ll do it again.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    11,138
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    182
    + +
    Kudos:
    +
    4,038
    + +
    Bookmarks:
    +
    647
    + +
    Hits:
    +
    56,909
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/astolat/pseuds/shalott/works.html b/tests/mocks/data/ao3/users/astolat/pseuds/shalott/works.html new file mode 100644 index 00000000..1b1f7d94 --- /dev/null +++ b/tests/mocks/data/ao3/users/astolat/pseuds/shalott/works.html @@ -0,0 +1,990 @@ + + + + + + + + + + + + + + + + shalott - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 Work by shalott (astolat) +

+ + + + + + + + + + +

Listing Works

+
    +
  1. + +
    + +

    + [VID] The Mountain + by + + + , + + + + + +

    + +
    + Fandoms: + The Lord of the Rings (Movies) +   +
    + + + +

    18 Aug 2004

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Frodo and Aragorn's parallel journeys.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    16
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    15
    + +
    Kudos:
    +
    83
    + +
    Bookmarks:
    +
    12
    + +
    Hits:
    +
    1,947
    + +
    + + +
  2. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + + + +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/astolat/pseuds/the lady of shalott/works.html b/tests/mocks/data/ao3/users/astolat/pseuds/the lady of shalott/works.html new file mode 100644 index 00000000..2302b8f4 --- /dev/null +++ b/tests/mocks/data/ao3/users/astolat/pseuds/the lady of shalott/works.html @@ -0,0 +1,2330 @@ + + + + + + + + + + + + + + + + the lady of shalott - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 18 Works by the lady of shalott (astolat) +

+ + + + + + + + + + +

Listing Works

+
    +
  1. + +
    + +

    + Going Home + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    21 Mar 2003

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Angst-heavy sequel to Sentinel Too.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    10,707
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + + +
    Kudos:
    +
    58
    + +
    Bookmarks:
    +
    8
    + +
    Hits:
    +
    2,035
    + +
    + + +
  2. +
  3. + +
    + +

    + Sweet Surrender + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    20 Oct 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Originally published in the fanzine Resurrections. A Sentinel Too sequel. Yes, it's Sarah MacLachlan songfic.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    7,384
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    52
    + +
    Bookmarks:
    +
    3
    + +
    Hits:
    +
    1,519
    + +
    + + +
  4. +
  5. + +
    + +

    + Control + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    11 Sep 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    A missing scene (actually, more of a "what really happened" scene) set in the episode "Rogue", in the restaurant where Brackett scanned Jim and Blair for bugs and tracking devices.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    3,804
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    68
    + +
    Bookmarks:
    +
    4
    + +
    Hits:
    +
    1,866
    + +
    + + +
  6. +
  7. + +
    + +

    + A Model of Decorum + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    04 Aug 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    (Abandoned story!)

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    8,415
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    52
    + +
    Bookmarks:
    +
    3
    + +
    Hits:
    +
    1,021
    + +
    + + +
  8. +
  9. + +
    + +

    + Model Behavior + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    03 Aug 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    An AU where Jim (James) is a businessman and Blair the model his company hires. Originally published in the zine Love & Guns 6.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,424
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + + +
    Kudos:
    +
    55
    + +
    Bookmarks:
    +
    4
    + +
    Hits:
    +
    1,573
    + +
    + + +
  10. +
  11. + +
    + +

    + What Will It Be + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    09 Jul 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Songfic based on "I Don't Want to Wait" by Paula Cole.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,186
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + + +
    Kudos:
    +
    41
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    771
    + +
    + + +
  12. +
  13. + +
    + +

    + Dream Deferred + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    17 May 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Blair deals with the ramifications of his offer to destroy his work.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,907
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    98
    + +
    Bookmarks:
    +
    9
    + +
    Hits:
    +
    1,719
    + +
    + + +
  14. +
  15. + +
    + +

    + Wild Things + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    29 Apr 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Jim and Blair get marooned in an unfortunate location.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    4,651
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    60
    + +
    Bookmarks:
    +
    5
    + +
    Hits:
    +
    1,246
    + +
    + + +
  16. +
  17. + +
    + +

    + Sensual Rhythms + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    24 Apr 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Oshun takes an interest in the lives of a very clueless Sentinel and Guide.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    11,943
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + + +
    Kudos:
    +
    63
    + +
    Bookmarks:
    +
    9
    + +
    Hits:
    +
    1,900
    + +
    + + +
  18. +
  19. + +
    + +

    + New Territory + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    02 Apr 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    More fun with the Sentinel-Guide bond.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    9,803
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    196
    + +
    Bookmarks:
    +
    9
    + +
    Hits:
    +
    4,151
    + +
    + + +
  20. +
  21. + +
    + +

    + Something To Talk About + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    23 Mar 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Rumors are flying about Jim and Blair, much to their surprise.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,258
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    6
    + +
    Kudos:
    +
    215
    + +
    Bookmarks:
    +
    35
    + +
    Hits:
    +
    3,408
    + +
    + + +
  22. +
  23. + +
    + +

    + April Fools + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    08 Mar 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Jim gets fooled on April 1 -- and then so does Blair.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,279
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    57
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    1,048
    + +
    + + +
  24. +
  25. + +
    + +

    + Territoriality + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    03 Mar 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    A new detective joins the Major Crimes department, and Jim isn't happy about it.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    10,448
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    7
    + +
    Kudos:
    +
    248
    + +
    Bookmarks:
    +
    26
    + +
    Hits:
    +
    6,018
    + +
    + + +
  26. +
  27. + +
    + +

    + World Burning Down + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    01 Mar 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Someone on SENAD (minotaur, was it you?) said that Jim's

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    12,359
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + + +
    Kudos:
    +
    71
    + +
    Bookmarks:
    +
    5
    + +
    Hits:
    +
    1,819
    + +
    + + +
  28. +
  29. + +
    + +

    + Halftime + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    23 Feb 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    This was going to be a silly PWP inspired by the nickname "Gupcake" for Blair, but out of the blue, it turned into a fairly angsty and complicated sequel to "Super Bowl Surprise".

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,288
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    42
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    816
    + +
    + + +
  30. +
  31. + +
    + +

    + New York, New York + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    17 Feb 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Jim and Blair in the big city.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    7,004
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    52
    + +
    Bookmarks:
    +
    6
    + +
    Hits:
    +
    1,053
    + +
    + + +
  32. +
  33. + +
    + +

    + Super Bowl Surprise + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    09 Feb 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Silly little PWP, just for fun. Warnings: Joel Taggart and gold lamé mentioned in same context.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,489
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    36
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    1,000
    + +
    + + +
  34. +
  35. + +
    + +

    + Breathing Room + by + + + + + + + + +

    + +
    + Fandoms: + The Sentinel +   +
    + + + +

    27 Jan 1998

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    This was my very first Sentinel story. Jim and Blair end up in a flophouse for reasons that do not need exploring at this juncture. The usual ensues. (Where by "the usual" I of course mean "gay sex".)

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,909
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    92
    + +
    Bookmarks:
    +
    6
    + +
    Hits:
    +
    1,470
    + +
    + + +
  36. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + + + +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/astolat/works.html b/tests/mocks/data/ao3/users/astolat/works.html new file mode 100644 index 00000000..fdc7d39d --- /dev/null +++ b/tests/mocks/data/ao3/users/astolat/works.html @@ -0,0 +1,2947 @@ + + + + + + + + + + + + + + + + astolat - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 535 Works by astolat +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + +
    + +

    + Research + by + + + + + + + + +

    + +
    + Fandoms: + Harry Potter - J. K. Rowling +   +
    + + + +

    21 Dec 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Ever since she’d been small, Hermione’s reward for good marks had been a rampage through a bookshop, so the day she tore open her O.W.L.s results and triumphantly brandished the eleven Outstandings, her beaming mother said, “We’ll go to Foyles tomorrow,” without any discussion required.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    36,337
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    613
    + +
    Kudos:
    +
    5,143
    + +
    Bookmarks:
    +
    1,620
    + +
    Hits:
    +
    49,162
    + +
    + + +
  2. +
  3. + +
    + +

    + Inheritance + by + + + + + + + + +

    + +
    + Fandoms: + Lucifer (TV) +   +
    + + + +

    21 Dec 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    He didn’t tell her, I’ll die if you leave me, even though it was quite literally true.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    3,921
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    65
    + +
    Kudos:
    +
    925
    + +
    Bookmarks:
    +
    81
    + +
    Hits:
    +
    5,153
    + +
    + + +
  4. +
  5. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Legolas looked at the Dwarf to dislike, when first the Fellowship left Rivendell, and was satisfied with everything he saw. To the memory-shadow of Tauriel that lived yet in his mind, he listed the faults one after another. Did you truly love such a one? he asked her silently, disdainful.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,825
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    60
    + +
    Kudos:
    +
    1,312
    + +
    Bookmarks:
    +
    111
    + +
    Hits:
    +
    6,743
    + +
    + + +
  6. +
  7. + +
    + +

    + Hallelujah + by + + + + + + + + +

    + +
    + Fandoms: + Good Omens (TV) +   +
    + + + +

    18 Dec 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    This most wonderful and unexpected of all days, the first day after the end of the world.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    848
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    62
    + +
    Kudos:
    +
    864
    + +
    Bookmarks:
    +
    54
    + +
    Hits:
    +
    4,201
    + +
    + + +
  8. +
  9. + +
    + +

    + Tourney Field + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    17 Dec 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    “A friend from Derry tells me that Lord Stark is entering the tourney at the Crossings,” Tyrion said.

    +

    “He’s what?” Jaime said incredulously. “His first tourney at forty?”

    +

    “Not that Lord Stark,” Tyrion said. “The young one. Robb, I think his name is.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,688
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    235
    + +
    Kudos:
    +
    3,025
    + +
    Bookmarks:
    +
    325
    + +
    Hits:
    +
    20,671
    + +
    + + +
  10. +
  11. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    He was dying down here a little piece at a time, and if it took too long to get him out, there wouldn’t be a lot left of the guy he remembered in the mirror.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    3,479
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    68
    + +
    Kudos:
    +
    942
    + +
    Bookmarks:
    +
    67
    + +
    Hits:
    +
    5,867
    + +
    + + +
  12. +
  13. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    “Where did you come from?” Dorian demanded.

    +

    “He was so loud,” Cole said, blinking at him with all that disingenuous blue. “She’s taken, hurt, hurting, and I have to bear it: holding a debt so large my life and soul can’t lift it, so I have to give my heart and wholeness, too. But they’re going to cut her off, let the magic all run out and leave the hollow shell to haunt me, and I have to bear that too, but it’s too high a price; I have to go to her.” He shrugged. “So I came to help him.”

    +

    “Let’s keep going,” Bull said.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,233
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    32
    + +
    Kudos:
    +
    399
    + +
    Bookmarks:
    +
    59
    + +
    Hits:
    +
    3,025
    + +
    + + +
  14. +
  15. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Bucky didn’t open his eyes: there was no imminent threat. The device on the other side of the bed was just chiming a wake-up call at Stark, who started complaining at Jarvis; shortly after that, Potts started complaining at Stark. “Oh my God,” she groaned. “Tony, I don’t believe you. How could you talk me into this? You know what happens when I drink too much champagne!”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    3,786
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    67
    + +
    Kudos:
    +
    1,004
    + +
    Bookmarks:
    +
    63
    + +
    Hits:
    +
    5,883
    + +
    + + +
  16. +
  17. + +
    + +

    + Common Knowledge + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    11 Jan 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    It had never occurred to Jaime once, in the three solid months he’d spent in the pen, that Robb Stark could make his imprisonment worse by giving him a bath, a warm bed, and some decent food to eat.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,721
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    290
    + +
    Kudos:
    +
    5,139
    + +
    Bookmarks:
    +
    761
    + +
    Hits:
    +
    53,253
    + +
    + + +
  18. +
  19. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    There is a land of the living and a land of the dead and the bridge is love, the only survival.
    —Thornton Wilder

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    176,760
    +
    Chapters:
    +
    23/23
    + + + +
    Comments:
    +
    3,627
    + +
    Kudos:
    +
    5,877
    + +
    Bookmarks:
    +
    1,610
    + +
    Hits:
    +
    184,727
    + +
    + + +
  20. +
  21. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    “The girl asks for more deaths than she is owed. The Many-Faced God may grant it. But for this, there will be a price. And a man cannot say what the price will be. A girl must pay. A man must pay. A girl’s brother must pay, if he agrees.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    12,034
    +
    Chapters:
    +
    2/2
    + + + +
    Comments:
    +
    309
    + +
    Kudos:
    +
    4,737
    + +
    Bookmarks:
    +
    851
    + +
    Hits:
    +
    75,687
    + +
    + + +
  22. +
  23. + +
    + +

    + A Man of Honor + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    14 Jan 2023

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    “Yes, Lord Stark has come to town to help his mother bring out his eldest sister,” Tyrion said, pouring Jaime a glass of wine. “I understand he also plans to take his seat in the House of the Law. His uncle Edmure has been gathering time for him to give his maiden speech.”

    “Yes, yes,” Jaime said irritably. “That’s not what matters and you know it. Well?”

    Tyrion sighed. “He's nineteen, well-favored, and as far as I know, neither demented nor stupid. And not a hint of a betrothal. Sorry.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    36,460
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    411
    + +
    Kudos:
    +
    5,314
    + +
    Bookmarks:
    +
    1,320
    + +
    Hits:
    +
    69,843
    + +
    + + +
  24. +
  25. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    The deep satisfaction of having made the right choice; of having found a clear-flowing wellspring of true honor to protect.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    61,498
    +
    Chapters:
    +
    9/9
    + + + +
    Comments:
    +
    1,384
    + +
    Kudos:
    +
    5,685
    + +
    Bookmarks:
    +
    1,255
    + +
    Hits:
    +
    111,695
    + +
    + + +
  26. +
  27. + +
    + +

    + Winter's Crown + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    13 Nov 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    “When the Night’s King rides,” the giant said, each word slow as cold honey pouring, “the King in the North must answer. The King in the North…whose name is Stark.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    76,073
    +
    Chapters:
    +
    13/13
    + + + +
    Comments:
    +
    2,313
    + +
    Kudos:
    +
    6,387
    + +
    Bookmarks:
    +
    1,596
    + +
    Hits:
    +
    123,542
    + +
    + + +
  28. +
  29. + +
    + +

    + Royal Flush + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    08 Jun 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Robb Stark had swept his entire hand of cards off the table, and Tyrion couldn’t see how to make a single play at all.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    85,124
    +
    Chapters:
    +
    16/16
    + + + +
    Comments:
    +
    3,283
    + +
    Kudos:
    +
    7,634
    + +
    Bookmarks:
    +
    2,037
    + +
    Hits:
    +
    200,757
    + +
    + + +
  30. +
  31. + +
    + +

    + Lifeline + by + + + + + + + + +

    + +
    + Fandoms: + Fast & Furious (Movies) +   +
    + + + +

    19 May 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Vampires dominate by biting, alpha wolves dominate by fucking. What the hell’s going on between the two of you, I don’t even have a clue."

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    37,951
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    344
    + +
    Kudos:
    +
    4,662
    + +
    Bookmarks:
    +
    1,295
    + +
    Hits:
    +
    58,401
    + +
    + + +
  32. +
  33. + +
    + +

    + Heal Thyself + by + + + + + + + + +

    + +
    + Fandoms: + Harry Potter - J. K. Rowling +   +
    + + + +

    07 Apr 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Are you going for the course?" Lovegood asked. "You have the NEWTs.”

    “What course?” Draco said, then, “No, don’t be ridiculous,” when he realized she meant the notice pinned up on the board he’d been staring at: Applicants To The Introductory Mediwizard Course For The Coming Term Shall Present Themselves In The Chief Mediwizard’s Office By August 24th.

    “Oh, I thought you might,” she said. “Well, goodbye.” And off she wandered again in her addled way.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    46,905
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    1,711
    + +
    Kudos:
    +
    26,892
    + +
    Bookmarks:
    +
    9,928
    + +
    Hits:
    +
    425,644
    + +
    + + +
  34. +
  35. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Jaime didn’t like the Starks at all. He didn’t like their earnestness and honesty and prudishness, he didn’t like their cold heavy-walled castle, he didn’t like their frozen block of a realm, and he especially didn’t like Robb Stark, who had saved his son’s life and been kind to him and got him to sleep, and what business of his was it to have done any of those things?

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    26,976
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    499
    + +
    Kudos:
    +
    7,550
    + +
    Bookmarks:
    +
    2,095
    + +
    Hits:
    +
    94,792
    + +
    + + +
  36. +
  37. + +
    + +

    + Paying Debts + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    23 Feb 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Tywin gave her a stern look. “Watch your tongue.”

    “Or what, you’ll cut my head off, like your grandson did to my father?” Arya spat at him, a cub trying to bite with milk teeth.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    7,667
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    510
    + +
    Kudos:
    +
    8,345
    + +
    Bookmarks:
    +
    1,702
    + +
    Hits:
    +
    85,449
    + +
    + + +
  38. +
  39. + +
    + +

    + The Next Time + by + + + + + + + + +

    + +
    + Fandoms: + Game of Thrones (TV) +   +
    + + + +

    26 Jan 2022

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Jaime looked back at Robb with rage blazing through him and said, low and utterly flat, “The next time I see you, Stark, I’ll kill you.”

    “The next time I see you, Ser Jaime,” Robb said, “if you ask nicely, I’ll do it again.”

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    11,138
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    182
    + +
    Kudos:
    +
    4,037
    + +
    Bookmarks:
    +
    647
    + +
    Hits:
    +
    56,875
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/chemicalcain/pseuds/chemicalcain/works.html b/tests/mocks/data/ao3/users/chemicalcain/pseuds/chemicalcain/works.html new file mode 100644 index 00000000..d16e530d --- /dev/null +++ b/tests/mocks/data/ao3/users/chemicalcain/pseuds/chemicalcain/works.html @@ -0,0 +1,2863 @@ + + + + + + + + + + + + + + + + chemicalcain - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 34 Works by chemicalcain +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + +
    + +

    + Mountains + by + + + + + + + + +

    + +
    + Fandoms: + Control (Remedy Video Games) +   +
    + + + +

    27 Sep 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Something is wrong inside the Oldest House. The House knows this.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    150
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    52
    + +
    + + +
  2. +
  3. + +
    + +

    + Nightmare Horse + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    27 Sep 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The newest Listener meets the oldest horse.

    +

    (Set during "The Cure for Madness.")

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    3
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    3
    + +
    + + +
  4. +
  5. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Inspired by Season 1 episode 13.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    86
    + +
    + + +
  6. +
  7. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    One for One is a card about exchange and sacrifice. The Seal demands a soul.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    6
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    58
    + +
    + + +
  8. +
  9. + +
    + +

    + Blasphemy + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls III: Morrowind, Elder Scrolls +   +
    + + + +

    20 Aug 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "What?" Aryn propped herself up on the pillow and smirked. "There's a whole sermon about Vivec and Molag Bal getting it on. I'm not being any hornier than the Poet-King."

    +

    Or: The events that led to Aryn leaving Cyrodiil.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    555
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    2
    + + +
    Hits:
    +
    19
    + +
    + + +
  10. +
  11. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Pat tries to help Brian understand a new kind of wrestling.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,670
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    14
    + + +
    Hits:
    +
    184
    + +
    + + +
  12. +
  13. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Atlantis is back, and they're really into full contact nude sports. The world is wild about it.

    +

    Fuckathlon is an open AU setting for fanfiction or original works. Chapter 1 will detail the core premise, and chapter 2 will pose questions for you to dig into the setting and figure out how to put your favorite weirdos in it.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,119
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    4
    + +
    Hits:
    +
    175
    + +
    + + +
  14. +
  15. + +
    + +

    + As Sweet As A Song, As Right As A Wrong + by + + + + + + + for hobbitdragon + + +

    + +
    + Fandoms: + Original Work +   +
    + + + +

    11 Aug 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Do you want to be a monster?

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,752
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    12
    + +
    Kudos:
    +
    196
    + +
    Bookmarks:
    +
    18
    + +
    Hits:
    +
    4,526
    + +
    + + +
  16. +
  17. + +
    + +

    + Beauty + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    28 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Ingrid has been under pressure to emphasize her femininity. She talks it out with Felix over a spar.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    7
    + + +
    Hits:
    +
    66
    + +
    + + +
  18. +
  19. + +
    + +

    + Self-Reflection + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    21 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Okay," Yugi says, reaching out to touch the mirror. "I think I get how this works. You only come out when there's some kind of challenge. So let's play a game. Just you and me. A shadow game."

    His reflection smiles, and in a blink, the room goes dark.

    ---

    Set in season 0. Yugi tries to talk to the spirit for the first time.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    784
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    19
    + +
    Kudos:
    +
    198
    + +
    Bookmarks:
    +
    31
    + +
    Hits:
    +
    1,478
    + +
    + + +
  20. +
  21. + +
    + +

    + Curse-of-Dreams + by + + + + + + + for Allekha + + +

    + +
    + Fandoms: + Elder Scrolls III: Morrowind +   +
    + + + +

    08 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    When an Ashlander contracts Corprus and is exiled from the tribe, servants of the Sixth House seize the opportunity to increase their ranks. The new recruit begins to have recurring dreams of the Nerevarine... or several Nerevarines.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,231
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    9
    + + +
    Hits:
    +
    28
    + +
    + + +
  22. +
  23. + +
    + +

    + olin lipu + by + + + + + + + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    28 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The Exodia scene from the 4kids dub, but in toki pona.

    Chapter 1 is the transcript in toki pona and screenshots with captions in sitelen pona. Chapter 2 is transliterations with translation notes. Enjoy!

    +
    + + + + +
    +
    Language:
    +
    toki pona
    +
    Words:
    +
    831
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    12
    + + +
    Hits:
    +
    110
    + +
    + + +
  24. +
  25. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Something goes wrong with Lysithea during a battle. Hanneman suggests a medical procedure to investigate. It goes badly.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,135
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    3
    + + +
    Hits:
    +
    48
    + +
    + + +
  26. +
  27. + +
    + +

    + Chiaroscuro + by + + + + + + + for VThinksOn + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    20 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Yugi's limbs move faster than his mind can track. Before he knows it, both of his assailants are on the ground under the heel of studded leather platform boots, which Yugi definitely wasn't wearing when he left the house this morning.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,412
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    34
    + +
    Bookmarks:
    +
    8
    + +
    Hits:
    +
    312
    + +
    + + +
  28. +
  29. + +
    + +

    + Who Can I Turn To? + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    21 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    She looks nothing like her older self did when she gave her the notebook and started The Mission. Her hair is cropped short, fluffy and dyed black with a red streak hanging across her eyes, and she's wearing a plaid Hot Topic skirt and fingerless gloves, and she just fixed her eyeliner, and besides, Future Beth must have been at least 25. If Serene is looking for her yet, he's looking for a soldier, not a teenager in fishnets.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,952
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    100
    + +
    + + +
  30. +
  31. + +
    + +

    + Jaunt + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    07 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Experimental piece: Schrödinger Diary from the perspective of the mouse.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + + + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    19
    + +
    + + +
  32. +
  33. + +
    + +

    + Chosen + by + + + + + + + + +

    + +
    + Fandoms: + The Silt Verses (Podcast) +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,164
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    14
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    78
    + +
    + + +
  34. +
  35. + +
    + +

    + Love Me or Leave Me + by + + + + + + + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    There's a definite pace to Linhardt's strokes now, and he opens his eyes to meet Lysithea's gaze dead-on. "You can leave if you want."

    She can't think of a response to that, not with his piercing eyes on hers, not when she can see those slender fingers moving in such an obscene way. He's right; she could leave as easily as she'd walked in. She just doesn't want to, and Linhardt knows it, the smug bastard.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,595
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    16
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    255
    + +
    + + +
  36. +
  37. + +
    + +

    + Reckless Recreational Spell Use + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    05 Mar 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Magic allows for many exciting possibilities during sex, but most of them come with risks. Colette posts a bulletin about safe sex, which receives several responses.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    600
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    7
    + +
    Kudos:
    +
    20
    + +
    Bookmarks:
    +
    3
    + +
    Hits:
    +
    136
    + +
    + + +
  38. +
  39. + +
    + +

    + Impossible Claims + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls, Elder Scrolls V: Skyrim +   +
    + + + +

    20 Feb 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "I do not enjoy the cavalier and sarcastic manner with which the author frames his study. While there is valuable and intelligent insight into Dunmer beliefs, the author makes impossible claims about his life as almost marginal comments. See below, where he claims to have had tea with the First Era Dunmer ruler, Nerevar..."

    Mzunchak responds to Calcelmo's review of his latest work.

    (4E 187)

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    755
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    41
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/chemicalcain/pseuds/chemicalcain/works/work_search[sort_column]=hits/01.html b/tests/mocks/data/ao3/users/chemicalcain/pseuds/chemicalcain/works/work_search[sort_column]=hits/01.html new file mode 100644 index 00000000..73656554 --- /dev/null +++ b/tests/mocks/data/ao3/users/chemicalcain/pseuds/chemicalcain/works/work_search[sort_column]=hits/01.html @@ -0,0 +1,2863 @@ + + + + + + + + + + + + + + + + chemicalcain - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 34 Works by chemicalcain +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + +
    + +

    + As Sweet As A Song, As Right As A Wrong + by + + + + + + + for hobbitdragon + + +

    + +
    + Fandoms: + Original Work +   +
    + + + +

    11 Aug 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Do you want to be a monster?

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,752
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    12
    + +
    Kudos:
    +
    196
    + +
    Bookmarks:
    +
    18
    + +
    Hits:
    +
    4,526
    + +
    + + +
  2. +
  3. + +
    + +

    + Cravats For Kravitz + by + + + + + + + + +

    + +
    + Fandoms: + The Adventure Zone (Podcast) +   +
    + + + +

    27 Nov 2016

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Taako's drunk and upset. Magnus's obvious solution is to form a band.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,548
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    18
    + +
    Kudos:
    +
    195
    + +
    Bookmarks:
    +
    11
    + +
    Hits:
    +
    1,880
    + +
    + + +
  4. +
  5. + +
    + +

    + Self-Reflection + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    21 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Okay," Yugi says, reaching out to touch the mirror. "I think I get how this works. You only come out when there's some kind of challenge. So let's play a game. Just you and me. A shadow game."

    His reflection smiles, and in a blink, the room goes dark.

    ---

    Set in season 0. Yugi tries to talk to the spirit for the first time.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    784
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    19
    + +
    Kudos:
    +
    198
    + +
    Bookmarks:
    +
    31
    + +
    Hits:
    +
    1,478
    + +
    + + +
  6. +
  7. + +
    + +

    + Thirty Clove + by + + + + + + + + +

    + +
    + Fandoms: + The Adventure Zone (Podcast) +   +
    + + + +

    08 Nov 2016

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    (Post Eleventh Hour, so, spoilers.) Taako's dealing with what he learned in Refuge, and he's reliving it all over again.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,068
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    149
    + +
    Bookmarks:
    +
    9
    + +
    Hits:
    +
    1,211
    + +
    + + +
  8. +
  9. + +
    + +

    + Hey Jealousy + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    25 Nov 2018

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    + "if you don't expect too much from me, you might not be let down" +

     

    Paul comes home not long after graduation and receives an unexpected visit from an estranged friend.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    4,777
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    24
    + +
    Bookmarks:
    +
    5
    + +
    Hits:
    +
    542
    + +
    + + +
  10. +
  11. + +
    + +

    + Go to bed + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    17 Mar 2017

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    When Jack gets home from a late shift, Paul's still up.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    937
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    3
    + +
    Kudos:
    +
    27
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    382
    + +
    + + +
  12. +
  13. + +
    + +

    + Chiaroscuro + by + + + + + + + for VThinksOn + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    20 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Yugi's limbs move faster than his mind can track. Before he knows it, both of his assailants are on the ground under the heel of studded leather platform boots, which Yugi definitely wasn't wearing when he left the house this morning.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,412
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    34
    + +
    Bookmarks:
    +
    8
    + +
    Hits:
    +
    312
    + +
    + + +
  14. +
  15. + +
    + +

    + Love Me or Leave Me + by + + + + + + + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    There's a definite pace to Linhardt's strokes now, and he opens his eyes to meet Lysithea's gaze dead-on. "You can leave if you want."

    She can't think of a response to that, not with his piercing eyes on hers, not when she can see those slender fingers moving in such an obscene way. He's right; she could leave as easily as she'd walked in. She just doesn't want to, and Linhardt knows it, the smug bastard.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,595
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    16
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    255
    + +
    + + +
  16. +
  17. + +
    + +

    + Keeping Warm + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    28 Nov 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The College's architecture leaves much to be desired, including doors.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    21
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    186
    + +
    + + +
  18. +
  19. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Pat tries to help Brian understand a new kind of wrestling.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,670
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    14
    + + +
    Hits:
    +
    184
    + +
    + + +
  20. +
  21. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Atlantis is back, and they're really into full contact nude sports. The world is wild about it.

    +

    Fuckathlon is an open AU setting for fanfiction or original works. Chapter 1 will detail the core premise, and chapter 2 will pose questions for you to dig into the setting and figure out how to put your favorite weirdos in it.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,119
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    4
    + +
    Hits:
    +
    175
    + +
    + + +
  22. +
  23. + +
    + +

    + Reckless Recreational Spell Use + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    05 Mar 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Magic allows for many exciting possibilities during sex, but most of them come with risks. Colette posts a bulletin about safe sex, which receives several responses.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    600
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    7
    + +
    Kudos:
    +
    20
    + +
    Bookmarks:
    +
    3
    + +
    Hits:
    +
    136
    + +
    + + +
  24. +
  25. + +
    + +

    + olin lipu + by + + + + + + + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    28 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The Exodia scene from the 4kids dub, but in toki pona.

    Chapter 1 is the transcript in toki pona and screenshots with captions in sitelen pona. Chapter 2 is transliterations with translation notes. Enjoy!

    +
    + + + + +
    +
    Language:
    +
    toki pona
    +
    Words:
    +
    831
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    12
    + + +
    Hits:
    +
    110
    + +
    + + +
  26. +
  27. + +
    + +

    + Who Can I Turn To? + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    21 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    She looks nothing like her older self did when she gave her the notebook and started The Mission. Her hair is cropped short, fluffy and dyed black with a red streak hanging across her eyes, and she's wearing a plaid Hot Topic skirt and fingerless gloves, and she just fixed her eyeliner, and besides, Future Beth must have been at least 25. If Serene is looking for her yet, he's looking for a soldier, not a teenager in fishnets.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,952
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    100
    + +
    + + +
  28. +
  29. + +
    + +

    + Bold + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    06 Dec 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    J'zargo, claiming to be the Dragonborn, approaches the master of the Greybeards.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    401
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    10
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    99
    + +
    + + +
  30. +
  31. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Inspired by Season 1 episode 13.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    86
    + +
    + + +
  32. +
  33. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    After a pretty serious fight in which Raz'elen gets pinned to a wall by a lance through the gut, he and Luvo are unable to sleep and have a bit of a conversation.

    An abridged and edited version of a text RP between cmdonovann and chemicalcain. Raz's parts of the RP are cmdonovann's, and Luvo's parts are chemicalcain's. Editing primarily done by cmdonovann.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    11,443
    +
    Chapters:
    +
    2/2
    + + + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    2
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    82
    + +
    + + +
  34. +
  35. + +
    + +

    + Chosen + by + + + + + + + + +

    + +
    + Fandoms: + The Silt Verses (Podcast) +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,164
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    14
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    78
    + +
    + + +
  36. +
  37. + +
    + +

    + Food crimes + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    06 Oct 2023

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Circa 2002, teenage Jack sometimes stays over at Paul's house for the weekend, then they get ready for school together on Monday morning. Today Jack witnesses Paul's breakfast choices.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    411
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    11
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    68
    + +
    + + +
  38. +
  39. + +
    + +

    + Beauty + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    28 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Ingrid has been under pressure to emphasize her femininity. She talks it out with Felix over a spar.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    7
    + + +
    Hits:
    +
    66
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/chemicalcain/works.html b/tests/mocks/data/ao3/users/chemicalcain/works.html new file mode 100644 index 00000000..df796258 --- /dev/null +++ b/tests/mocks/data/ao3/users/chemicalcain/works.html @@ -0,0 +1,2863 @@ + + + + + + + + + + + + + + + + chemicalcain - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 34 Works by chemicalcain +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + +
    + +

    + Mountains + by + + + + + + + + +

    + +
    + Fandoms: + Control (Remedy Video Games) +   +
    + + + +

    27 Sep 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Something is wrong inside the Oldest House. The House knows this.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    150
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    52
    + +
    + + +
  2. +
  3. + +
    + +

    + Nightmare Horse + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    27 Sep 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The newest Listener meets the oldest horse.

    +

    (Set during "The Cure for Madness.")

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    3
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    3
    + +
    + + +
  4. +
  5. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Inspired by Season 1 episode 13.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    86
    + +
    + + +
  6. +
  7. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    One for One is a card about exchange and sacrifice. The Seal demands a soul.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    6
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    58
    + +
    + + +
  8. +
  9. + +
    + +

    + Blasphemy + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls III: Morrowind, Elder Scrolls +   +
    + + + +

    20 Aug 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "What?" Aryn propped herself up on the pillow and smirked. "There's a whole sermon about Vivec and Molag Bal getting it on. I'm not being any hornier than the Poet-King."

    +

    Or: The events that led to Aryn leaving Cyrodiil.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    555
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    2
    + + +
    Hits:
    +
    19
    + +
    + + +
  10. +
  11. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Pat tries to help Brian understand a new kind of wrestling.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,670
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    14
    + + +
    Hits:
    +
    184
    + +
    + + +
  12. +
  13. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Atlantis is back, and they're really into full contact nude sports. The world is wild about it.

    +

    Fuckathlon is an open AU setting for fanfiction or original works. Chapter 1 will detail the core premise, and chapter 2 will pose questions for you to dig into the setting and figure out how to put your favorite weirdos in it.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,119
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    4
    + +
    Hits:
    +
    175
    + +
    + + +
  14. +
  15. + +
    + +

    + As Sweet As A Song, As Right As A Wrong + by + + + + + + + for hobbitdragon + + +

    + +
    + Fandoms: + Original Work +   +
    + + + +

    11 Aug 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Do you want to be a monster?

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,752
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    12
    + +
    Kudos:
    +
    196
    + +
    Bookmarks:
    +
    18
    + +
    Hits:
    +
    4,526
    + +
    + + +
  16. +
  17. + +
    + +

    + Beauty + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    28 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Ingrid has been under pressure to emphasize her femininity. She talks it out with Felix over a spar.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    7
    + + +
    Hits:
    +
    66
    + +
    + + +
  18. +
  19. + +
    + +

    + Self-Reflection + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    21 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Okay," Yugi says, reaching out to touch the mirror. "I think I get how this works. You only come out when there's some kind of challenge. So let's play a game. Just you and me. A shadow game."

    His reflection smiles, and in a blink, the room goes dark.

    ---

    Set in season 0. Yugi tries to talk to the spirit for the first time.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    784
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    19
    + +
    Kudos:
    +
    198
    + +
    Bookmarks:
    +
    31
    + +
    Hits:
    +
    1,478
    + +
    + + +
  20. +
  21. + +
    + +

    + Curse-of-Dreams + by + + + + + + + for Allekha + + +

    + +
    + Fandoms: + Elder Scrolls III: Morrowind +   +
    + + + +

    08 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    When an Ashlander contracts Corprus and is exiled from the tribe, servants of the Sixth House seize the opportunity to increase their ranks. The new recruit begins to have recurring dreams of the Nerevarine... or several Nerevarines.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,231
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    9
    + + +
    Hits:
    +
    28
    + +
    + + +
  22. +
  23. + +
    + +

    + olin lipu + by + + + + + + + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    28 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The Exodia scene from the 4kids dub, but in toki pona.

    Chapter 1 is the transcript in toki pona and screenshots with captions in sitelen pona. Chapter 2 is transliterations with translation notes. Enjoy!

    +
    + + + + +
    +
    Language:
    +
    toki pona
    +
    Words:
    +
    831
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    12
    + + +
    Hits:
    +
    110
    + +
    + + +
  24. +
  25. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Something goes wrong with Lysithea during a battle. Hanneman suggests a medical procedure to investigate. It goes badly.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,135
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    3
    + + +
    Hits:
    +
    48
    + +
    + + +
  26. +
  27. + +
    + +

    + Chiaroscuro + by + + + + + + + for VThinksOn + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    20 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Yugi's limbs move faster than his mind can track. Before he knows it, both of his assailants are on the ground under the heel of studded leather platform boots, which Yugi definitely wasn't wearing when he left the house this morning.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,412
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    34
    + +
    Bookmarks:
    +
    8
    + +
    Hits:
    +
    312
    + +
    + + +
  28. +
  29. + +
    + +

    + Who Can I Turn To? + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    21 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    She looks nothing like her older self did when she gave her the notebook and started The Mission. Her hair is cropped short, fluffy and dyed black with a red streak hanging across her eyes, and she's wearing a plaid Hot Topic skirt and fingerless gloves, and she just fixed her eyeliner, and besides, Future Beth must have been at least 25. If Serene is looking for her yet, he's looking for a soldier, not a teenager in fishnets.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,952
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    100
    + +
    + + +
  30. +
  31. + +
    + +

    + Jaunt + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    07 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Experimental piece: Schrödinger Diary from the perspective of the mouse.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + + + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    19
    + +
    + + +
  32. +
  33. + +
    + +

    + Chosen + by + + + + + + + + +

    + +
    + Fandoms: + The Silt Verses (Podcast) +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,164
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    14
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    78
    + +
    + + +
  34. +
  35. + +
    + +

    + Love Me or Leave Me + by + + + + + + + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    There's a definite pace to Linhardt's strokes now, and he opens his eyes to meet Lysithea's gaze dead-on. "You can leave if you want."

    She can't think of a response to that, not with his piercing eyes on hers, not when she can see those slender fingers moving in such an obscene way. He's right; she could leave as easily as she'd walked in. She just doesn't want to, and Linhardt knows it, the smug bastard.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,595
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    16
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    255
    + +
    + + +
  36. +
  37. + +
    + +

    + Reckless Recreational Spell Use + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    05 Mar 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Magic allows for many exciting possibilities during sex, but most of them come with risks. Colette posts a bulletin about safe sex, which receives several responses.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    600
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    7
    + +
    Kudos:
    +
    20
    + +
    Bookmarks:
    +
    3
    + +
    Hits:
    +
    136
    + +
    + + +
  38. +
  39. + +
    + +

    + Impossible Claims + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls, Elder Scrolls V: Skyrim +   +
    + + + +

    20 Feb 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "I do not enjoy the cavalier and sarcastic manner with which the author frames his study. While there is valuable and intelligent insight into Dunmer beliefs, the author makes impossible claims about his life as almost marginal comments. See below, where he claims to have had tea with the First Era Dunmer ruler, Nerevar..."

    Mzunchak responds to Calcelmo's review of his latest work.

    (4E 187)

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    755
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    41
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=created_at/01.html b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=created_at/01.html new file mode 100644 index 00000000..8c237325 --- /dev/null +++ b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=created_at/01.html @@ -0,0 +1,2863 @@ + + + + + + + + + + + + + + + + chemicalcain - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 34 Works by chemicalcain +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + +
    + +

    + Mountains + by + + + + + + + + +

    + +
    + Fandoms: + Control (Remedy Video Games) +   +
    + + + +

    27 Sep 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Something is wrong inside the Oldest House. The House knows this.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    150
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    52
    + +
    + + +
  2. +
  3. + +
    + +

    + Nightmare Horse + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    27 Sep 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The newest Listener meets the oldest horse.

    +

    (Set during "The Cure for Madness.")

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    3
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    3
    + +
    + + +
  4. +
  5. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Inspired by Season 1 episode 13.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    86
    + +
    + + +
  6. +
  7. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    One for One is a card about exchange and sacrifice. The Seal demands a soul.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    6
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    58
    + +
    + + +
  8. +
  9. + +
    + +

    + Blasphemy + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls III: Morrowind, Elder Scrolls +   +
    + + + +

    20 Aug 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "What?" Aryn propped herself up on the pillow and smirked. "There's a whole sermon about Vivec and Molag Bal getting it on. I'm not being any hornier than the Poet-King."

    +

    Or: The events that led to Aryn leaving Cyrodiil.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    555
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    2
    + + +
    Hits:
    +
    19
    + +
    + + +
  10. +
  11. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Pat tries to help Brian understand a new kind of wrestling.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,670
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    14
    + + +
    Hits:
    +
    184
    + +
    + + +
  12. +
  13. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Atlantis is back, and they're really into full contact nude sports. The world is wild about it.

    +

    Fuckathlon is an open AU setting for fanfiction or original works. Chapter 1 will detail the core premise, and chapter 2 will pose questions for you to dig into the setting and figure out how to put your favorite weirdos in it.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,119
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    4
    + +
    Hits:
    +
    175
    + +
    + + +
  14. +
  15. + +
    + +

    + As Sweet As A Song, As Right As A Wrong + by + + + + + + + for hobbitdragon + + +

    + +
    + Fandoms: + Original Work +   +
    + + + +

    11 Aug 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Do you want to be a monster?

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,752
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    12
    + +
    Kudos:
    +
    196
    + +
    Bookmarks:
    +
    18
    + +
    Hits:
    +
    4,526
    + +
    + + +
  16. +
  17. + +
    + +

    + Beauty + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    28 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Ingrid has been under pressure to emphasize her femininity. She talks it out with Felix over a spar.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    7
    + + +
    Hits:
    +
    66
    + +
    + + +
  18. +
  19. + +
    + +

    + Self-Reflection + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    21 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Okay," Yugi says, reaching out to touch the mirror. "I think I get how this works. You only come out when there's some kind of challenge. So let's play a game. Just you and me. A shadow game."

    His reflection smiles, and in a blink, the room goes dark.

    ---

    Set in season 0. Yugi tries to talk to the spirit for the first time.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    784
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    19
    + +
    Kudos:
    +
    198
    + +
    Bookmarks:
    +
    31
    + +
    Hits:
    +
    1,478
    + +
    + + +
  20. +
  21. + +
    + +

    + Curse-of-Dreams + by + + + + + + + for Allekha + + +

    + +
    + Fandoms: + Elder Scrolls III: Morrowind +   +
    + + + +

    08 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    When an Ashlander contracts Corprus and is exiled from the tribe, servants of the Sixth House seize the opportunity to increase their ranks. The new recruit begins to have recurring dreams of the Nerevarine... or several Nerevarines.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,231
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    9
    + + +
    Hits:
    +
    28
    + +
    + + +
  22. +
  23. + +
    + +

    + olin lipu + by + + + + + + + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    28 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The Exodia scene from the 4kids dub, but in toki pona.

    Chapter 1 is the transcript in toki pona and screenshots with captions in sitelen pona. Chapter 2 is transliterations with translation notes. Enjoy!

    +
    + + + + +
    +
    Language:
    +
    toki pona
    +
    Words:
    +
    831
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    12
    + + +
    Hits:
    +
    110
    + +
    + + +
  24. +
  25. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Something goes wrong with Lysithea during a battle. Hanneman suggests a medical procedure to investigate. It goes badly.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,135
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    3
    + + +
    Hits:
    +
    48
    + +
    + + +
  26. +
  27. + +
    + +

    + Chiaroscuro + by + + + + + + + for VThinksOn + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    20 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Yugi's limbs move faster than his mind can track. Before he knows it, both of his assailants are on the ground under the heel of studded leather platform boots, which Yugi definitely wasn't wearing when he left the house this morning.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,412
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    34
    + +
    Bookmarks:
    +
    8
    + +
    Hits:
    +
    312
    + +
    + + +
  28. +
  29. + +
    + +

    + Who Can I Turn To? + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    21 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    She looks nothing like her older self did when she gave her the notebook and started The Mission. Her hair is cropped short, fluffy and dyed black with a red streak hanging across her eyes, and she's wearing a plaid Hot Topic skirt and fingerless gloves, and she just fixed her eyeliner, and besides, Future Beth must have been at least 25. If Serene is looking for her yet, he's looking for a soldier, not a teenager in fishnets.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,952
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    100
    + +
    + + +
  30. +
  31. + +
    + +

    + Jaunt + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    07 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Experimental piece: Schrödinger Diary from the perspective of the mouse.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + + + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    19
    + +
    + + +
  32. +
  33. + +
    + +

    + Chosen + by + + + + + + + + +

    + +
    + Fandoms: + The Silt Verses (Podcast) +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,164
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    14
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    78
    + +
    + + +
  34. +
  35. + +
    + +

    + Love Me or Leave Me + by + + + + + + + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    There's a definite pace to Linhardt's strokes now, and he opens his eyes to meet Lysithea's gaze dead-on. "You can leave if you want."

    She can't think of a response to that, not with his piercing eyes on hers, not when she can see those slender fingers moving in such an obscene way. He's right; she could leave as easily as she'd walked in. She just doesn't want to, and Linhardt knows it, the smug bastard.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,595
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    16
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    255
    + +
    + + +
  36. +
  37. + +
    + +

    + Reckless Recreational Spell Use + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    05 Mar 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Magic allows for many exciting possibilities during sex, but most of them come with risks. Colette posts a bulletin about safe sex, which receives several responses.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    600
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    7
    + +
    Kudos:
    +
    20
    + +
    Bookmarks:
    +
    3
    + +
    Hits:
    +
    136
    + +
    + + +
  38. +
  39. + +
    + +

    + Impossible Claims + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls, Elder Scrolls V: Skyrim +   +
    + + + +

    20 Feb 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "I do not enjoy the cavalier and sarcastic manner with which the author frames his study. While there is valuable and intelligent insight into Dunmer beliefs, the author makes impossible claims about his life as almost marginal comments. See below, where he claims to have had tea with the First Era Dunmer ruler, Nerevar..."

    Mzunchak responds to Calcelmo's review of his latest work.

    (4E 187)

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    755
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    41
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=hits/01.html b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=hits/01.html new file mode 100644 index 00000000..a7e39a98 --- /dev/null +++ b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=hits/01.html @@ -0,0 +1,2863 @@ + + + + + + + + + + + + + + + + chemicalcain - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 34 Works by chemicalcain +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + +
    + +

    + As Sweet As A Song, As Right As A Wrong + by + + + + + + + for hobbitdragon + + +

    + +
    + Fandoms: + Original Work +   +
    + + + +

    11 Aug 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Do you want to be a monster?

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,752
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    12
    + +
    Kudos:
    +
    196
    + +
    Bookmarks:
    +
    18
    + +
    Hits:
    +
    4,526
    + +
    + + +
  2. +
  3. + +
    + +

    + Cravats For Kravitz + by + + + + + + + + +

    + +
    + Fandoms: + The Adventure Zone (Podcast) +   +
    + + + +

    27 Nov 2016

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Taako's drunk and upset. Magnus's obvious solution is to form a band.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,548
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    18
    + +
    Kudos:
    +
    195
    + +
    Bookmarks:
    +
    11
    + +
    Hits:
    +
    1,880
    + +
    + + +
  4. +
  5. + +
    + +

    + Self-Reflection + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    21 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Okay," Yugi says, reaching out to touch the mirror. "I think I get how this works. You only come out when there's some kind of challenge. So let's play a game. Just you and me. A shadow game."

    His reflection smiles, and in a blink, the room goes dark.

    ---

    Set in season 0. Yugi tries to talk to the spirit for the first time.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    784
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    19
    + +
    Kudos:
    +
    198
    + +
    Bookmarks:
    +
    31
    + +
    Hits:
    +
    1,478
    + +
    + + +
  6. +
  7. + +
    + +

    + Thirty Clove + by + + + + + + + + +

    + +
    + Fandoms: + The Adventure Zone (Podcast) +   +
    + + + +

    08 Nov 2016

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    (Post Eleventh Hour, so, spoilers.) Taako's dealing with what he learned in Refuge, and he's reliving it all over again.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,068
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    149
    + +
    Bookmarks:
    +
    9
    + +
    Hits:
    +
    1,211
    + +
    + + +
  8. +
  9. + +
    + +

    + Hey Jealousy + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    25 Nov 2018

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    + "if you don't expect too much from me, you might not be let down" +

     

    Paul comes home not long after graduation and receives an unexpected visit from an estranged friend.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    4,777
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    24
    + +
    Bookmarks:
    +
    5
    + +
    Hits:
    +
    542
    + +
    + + +
  10. +
  11. + +
    + +

    + Go to bed + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    17 Mar 2017

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    When Jack gets home from a late shift, Paul's still up.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    937
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    3
    + +
    Kudos:
    +
    27
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    382
    + +
    + + +
  12. +
  13. + +
    + +

    + Chiaroscuro + by + + + + + + + for VThinksOn + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    20 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Yugi's limbs move faster than his mind can track. Before he knows it, both of his assailants are on the ground under the heel of studded leather platform boots, which Yugi definitely wasn't wearing when he left the house this morning.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,412
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    34
    + +
    Bookmarks:
    +
    8
    + +
    Hits:
    +
    312
    + +
    + + +
  14. +
  15. + +
    + +

    + Love Me or Leave Me + by + + + + + + + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    There's a definite pace to Linhardt's strokes now, and he opens his eyes to meet Lysithea's gaze dead-on. "You can leave if you want."

    She can't think of a response to that, not with his piercing eyes on hers, not when she can see those slender fingers moving in such an obscene way. He's right; she could leave as easily as she'd walked in. She just doesn't want to, and Linhardt knows it, the smug bastard.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,595
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    16
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    255
    + +
    + + +
  16. +
  17. + +
    + +

    + Keeping Warm + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    28 Nov 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The College's architecture leaves much to be desired, including doors.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    21
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    186
    + +
    + + +
  18. +
  19. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Pat tries to help Brian understand a new kind of wrestling.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,670
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    14
    + + +
    Hits:
    +
    184
    + +
    + + +
  20. +
  21. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Atlantis is back, and they're really into full contact nude sports. The world is wild about it.

    +

    Fuckathlon is an open AU setting for fanfiction or original works. Chapter 1 will detail the core premise, and chapter 2 will pose questions for you to dig into the setting and figure out how to put your favorite weirdos in it.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,119
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    4
    + +
    Hits:
    +
    175
    + +
    + + +
  22. +
  23. + +
    + +

    + Reckless Recreational Spell Use + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    05 Mar 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Magic allows for many exciting possibilities during sex, but most of them come with risks. Colette posts a bulletin about safe sex, which receives several responses.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    600
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    7
    + +
    Kudos:
    +
    20
    + +
    Bookmarks:
    +
    3
    + +
    Hits:
    +
    136
    + +
    + + +
  24. +
  25. + +
    + +

    + olin lipu + by + + + + + + + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    28 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The Exodia scene from the 4kids dub, but in toki pona.

    Chapter 1 is the transcript in toki pona and screenshots with captions in sitelen pona. Chapter 2 is transliterations with translation notes. Enjoy!

    +
    + + + + +
    +
    Language:
    +
    toki pona
    +
    Words:
    +
    831
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    12
    + + +
    Hits:
    +
    110
    + +
    + + +
  26. +
  27. + +
    + +

    + Who Can I Turn To? + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    21 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    She looks nothing like her older self did when she gave her the notebook and started The Mission. Her hair is cropped short, fluffy and dyed black with a red streak hanging across her eyes, and she's wearing a plaid Hot Topic skirt and fingerless gloves, and she just fixed her eyeliner, and besides, Future Beth must have been at least 25. If Serene is looking for her yet, he's looking for a soldier, not a teenager in fishnets.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,952
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    100
    + +
    + + +
  28. +
  29. + +
    + +

    + Bold + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    06 Dec 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    J'zargo, claiming to be the Dragonborn, approaches the master of the Greybeards.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    401
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    10
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    99
    + +
    + + +
  30. +
  31. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Inspired by Season 1 episode 13.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    86
    + +
    + + +
  32. +
  33. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    After a pretty serious fight in which Raz'elen gets pinned to a wall by a lance through the gut, he and Luvo are unable to sleep and have a bit of a conversation.

    An abridged and edited version of a text RP between cmdonovann and chemicalcain. Raz's parts of the RP are cmdonovann's, and Luvo's parts are chemicalcain's. Editing primarily done by cmdonovann.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    11,443
    +
    Chapters:
    +
    2/2
    + + + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    2
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    82
    + +
    + + +
  34. +
  35. + +
    + +

    + Chosen + by + + + + + + + + +

    + +
    + Fandoms: + The Silt Verses (Podcast) +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,164
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    14
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    78
    + +
    + + +
  36. +
  37. + +
    + +

    + Food crimes + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    06 Oct 2023

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Circa 2002, teenage Jack sometimes stays over at Paul's house for the weekend, then they get ready for school together on Monday morning. Today Jack witnesses Paul's breakfast choices.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    411
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    11
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    68
    + +
    + + +
  38. +
  39. + +
    + +

    + Beauty + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    28 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Ingrid has been under pressure to emphasize her femininity. She talks it out with Felix over a spar.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    7
    + + +
    Hits:
    +
    66
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=title_to_sort_on/01.html b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=title_to_sort_on/01.html new file mode 100644 index 00000000..bda0663d --- /dev/null +++ b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=title_to_sort_on/01.html @@ -0,0 +1,2858 @@ + + + + + + + + + + + + + + + + chemicalcain - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 34 Works by chemicalcain +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + +
    + +

    + Armadillidiidae + by + + + + + + + + +

    + +
    + Fandoms: + Stargate SG-1 +   +
    + + + +

    05 Dec 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Teal'c is interested in Earth's fauna and agrees to go fishing with Jack.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    10
    + + +
    Hits:
    +
    38
    + +
    + + +
  2. +
  3. + +
    + +

    + As Sweet As A Song, As Right As A Wrong + by + + + + + + + for hobbitdragon + + +

    + +
    + Fandoms: + Original Work +   +
    + + + +

    11 Aug 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Do you want to be a monster?

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,752
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    12
    + +
    Kudos:
    +
    196
    + +
    Bookmarks:
    +
    18
    + +
    Hits:
    +
    4,526
    + +
    + + +
  4. +
  5. + +
    + +

    + Beauty + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    28 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Ingrid has been under pressure to emphasize her femininity. She talks it out with Felix over a spar.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    7
    + + +
    Hits:
    +
    66
    + +
    + + +
  6. +
  7. + +
    + +

    + Blasphemy + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls III: Morrowind, Elder Scrolls +   +
    + + + +

    20 Aug 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "What?" Aryn propped herself up on the pillow and smirked. "There's a whole sermon about Vivec and Molag Bal getting it on. I'm not being any hornier than the Poet-King."

    +

    Or: The events that led to Aryn leaving Cyrodiil.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    555
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    2
    + + +
    Hits:
    +
    19
    + +
    + + +
  8. +
  9. + +
    + +

    + Bold + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    06 Dec 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    J'zargo, claiming to be the Dragonborn, approaches the master of the Greybeards.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    401
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    10
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    99
    + +
    + + +
  10. +
  11. + +
    + +

    + Chiaroscuro + by + + + + + + + for VThinksOn + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    20 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Yugi's limbs move faster than his mind can track. Before he knows it, both of his assailants are on the ground under the heel of studded leather platform boots, which Yugi definitely wasn't wearing when he left the house this morning.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,412
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    34
    + +
    Bookmarks:
    +
    8
    + +
    Hits:
    +
    312
    + +
    + + +
  12. +
  13. + +
    + +

    + Chosen + by + + + + + + + + +

    + +
    + Fandoms: + The Silt Verses (Podcast) +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,164
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    14
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    78
    + +
    + + +
  14. +
  15. + +
    + +

    + Control + by + + + + + + + + +

    + +
    + Fandoms: + Original Work, Dungeons & Dragons (Roleplaying Game) +   +
    + + + +

    25 Feb 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    That distraction, that distance from his body, is why Luvo doesn't notice the cup on the ground in time. It crunches under his foot; he gives away their position and the target runs. The moment he recognizes his own failure is the moment the fury within him takes over. Luvo feels his grasp on the body slip completely as the transformation takes hold. All he can do is watch. Tribune Nimbus obliterates a Hell Knight in a single strike, and Luvo realizes he no longer has any power over his actions.

    ---

    Or, the first time Luvo fully dissociates from the body.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,345
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    1
    + + +
    Hits:
    +
    16
    + +
    + + +
  16. +
  17. + +
    + +

    + Cravats For Kravitz + by + + + + + + + + +

    + +
    + Fandoms: + The Adventure Zone (Podcast) +   +
    + + + +

    27 Nov 2016

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Taako's drunk and upset. Magnus's obvious solution is to form a band.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,548
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    18
    + +
    Kudos:
    +
    195
    + +
    Bookmarks:
    +
    11
    + +
    Hits:
    +
    1,880
    + +
    + + +
  18. +
  19. + +
    + +

    + Curse-of-Dreams + by + + + + + + + for Allekha + + +

    + +
    + Fandoms: + Elder Scrolls III: Morrowind +   +
    + + + +

    08 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    When an Ashlander contracts Corprus and is exiled from the tribe, servants of the Sixth House seize the opportunity to increase their ranks. The new recruit begins to have recurring dreams of the Nerevarine... or several Nerevarines.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,231
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    9
    + + +
    Hits:
    +
    28
    + +
    + + +
  20. +
  21. + +
    + +

    + Food crimes + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    06 Oct 2023

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Circa 2002, teenage Jack sometimes stays over at Paul's house for the weekend, then they get ready for school together on Monday morning. Today Jack witnesses Paul's breakfast choices.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    411
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    11
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    68
    + +
    + + +
  22. +
  23. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Atlantis is back, and they're really into full contact nude sports. The world is wild about it.

    +

    Fuckathlon is an open AU setting for fanfiction or original works. Chapter 1 will detail the core premise, and chapter 2 will pose questions for you to dig into the setting and figure out how to put your favorite weirdos in it.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,119
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    4
    + +
    Hits:
    +
    175
    + +
    + + +
  24. +
  25. + +
    + +

    + Go to bed + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    17 Mar 2017

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    When Jack gets home from a late shift, Paul's still up.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    937
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    3
    + +
    Kudos:
    +
    27
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    382
    + +
    + + +
  26. +
  27. + +
    + +

    + Hey Jealousy + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    25 Nov 2018

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    + "if you don't expect too much from me, you might not be let down" +

     

    Paul comes home not long after graduation and receives an unexpected visit from an estranged friend.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    4,777
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    24
    + +
    Bookmarks:
    +
    5
    + +
    Hits:
    +
    542
    + +
    + + +
  28. +
  29. + +
    + +

    + Impossible Claims + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls, Elder Scrolls V: Skyrim +   +
    + + + +

    20 Feb 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "I do not enjoy the cavalier and sarcastic manner with which the author frames his study. While there is valuable and intelligent insight into Dunmer beliefs, the author makes impossible claims about his life as almost marginal comments. See below, where he claims to have had tea with the First Era Dunmer ruler, Nerevar..."

    Mzunchak responds to Calcelmo's review of his latest work.

    (4E 187)

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    755
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    41
    + +
    + + +
  30. +
  31. + +
    + +

    + Inflict Wounds + by + + + , + + + + + +

    + +
    + Fandoms: + Original Work, Dungeons & Dragons (Roleplaying Game) +   +
    + + + +

    18 May 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Raz'elen tries to talk to Luvo about his feelings. Tribune Nimbus decides to intervene.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    7,689
    +
    Chapters:
    +
    1/1
    + + + + + +
    Hits:
    +
    46
    + +
    + + +
  32. +
  33. + +
    + +

    + Jaunt + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    07 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Experimental piece: Schrödinger Diary from the perspective of the mouse.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + + + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    19
    + +
    + + +
  34. +
  35. + +
    + +

    + Keeping Warm + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    28 Nov 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The College's architecture leaves much to be desired, including doors.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    21
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    186
    + +
    + + +
  36. +
  37. + +
    + +

    + Love Me or Leave Me + by + + + + + + + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    There's a definite pace to Linhardt's strokes now, and he opens his eyes to meet Lysithea's gaze dead-on. "You can leave if you want."

    She can't think of a response to that, not with his piercing eyes on hers, not when she can see those slender fingers moving in such an obscene way. He's right; she could leave as easily as she'd walked in. She just doesn't want to, and Linhardt knows it, the smug bastard.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,595
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    16
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    255
    + +
    + + +
  38. +
  39. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Pat tries to help Brian understand a new kind of wrestling.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,670
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    14
    + + +
    Hits:
    +
    184
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=title_to_sort_on__work_search[sort_direction]=desc/01.html b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=title_to_sort_on__work_search[sort_direction]=desc/01.html new file mode 100644 index 00000000..9f99d457 --- /dev/null +++ b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=title_to_sort_on__work_search[sort_direction]=desc/01.html @@ -0,0 +1,2867 @@ + + + + + + + + + + + + + + + + chemicalcain - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 34 Works by chemicalcain +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + +
    + +

    + Who Can I Turn To? + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    21 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    She looks nothing like her older self did when she gave her the notebook and started The Mission. Her hair is cropped short, fluffy and dyed black with a red streak hanging across her eyes, and she's wearing a plaid Hot Topic skirt and fingerless gloves, and she just fixed her eyeliner, and besides, Future Beth must have been at least 25. If Serene is looking for her yet, he's looking for a soldier, not a teenager in fishnets.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,952
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    100
    + +
    + + +
  2. +
  3. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Raz finally gets to talk to Nimbus alone.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    13,006
    +
    Chapters:
    +
    1/1
    + + + + + +
    Hits:
    +
    44
    + +
    + + +
  4. +
  5. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Something goes wrong with Lysithea during a battle. Hanneman suggests a medical procedure to investigate. It goes badly.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,135
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    3
    + + +
    Hits:
    +
    48
    + +
    + + +
  6. +
  7. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Co-occurring with Nothing to Say, but from Luvo's point of view. I strongly recommend reading Nothing to Say first, because the framing and author's notes will make the context of this scene much clearer.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,797
    +
    Chapters:
    +
    1/1
    + + + + +
    Kudos:
    +
    1
    + + +
    Hits:
    +
    16
    + +
    + + +
  8. +
  9. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Inspired by Season 1 episode 13.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    86
    + +
    + + +
  10. +
  11. + +
    + +

    + Thirty Clove + by + + + + + + + + +

    + +
    + Fandoms: + The Adventure Zone (Podcast) +   +
    + + + +

    08 Nov 2016

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    (Post Eleventh Hour, so, spoilers.) Taako's dealing with what he learned in Refuge, and he's reliving it all over again.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,068
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    149
    + +
    Bookmarks:
    +
    9
    + +
    Hits:
    +
    1,211
    + +
    + + +
  12. +
  13. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    After a pretty serious fight in which Raz'elen gets pinned to a wall by a lance through the gut, he and Luvo are unable to sleep and have a bit of a conversation.

    An abridged and edited version of a text RP between cmdonovann and chemicalcain. Raz's parts of the RP are cmdonovann's, and Luvo's parts are chemicalcain's. Editing primarily done by cmdonovann.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    11,443
    +
    Chapters:
    +
    2/2
    + + + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    2
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    82
    + +
    + + +
  14. +
  15. + +
    + +

    + Self-Reflection + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    21 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "Okay," Yugi says, reaching out to touch the mirror. "I think I get how this works. You only come out when there's some kind of challenge. So let's play a game. Just you and me. A shadow game."

    His reflection smiles, and in a blink, the room goes dark.

    ---

    Set in season 0. Yugi tries to talk to the spirit for the first time.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    784
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    19
    + +
    Kudos:
    +
    198
    + +
    Bookmarks:
    +
    31
    + +
    Hits:
    +
    1,478
    + +
    + + +
  16. +
  17. + +
    + +

    + Reckless Recreational Spell Use + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    05 Mar 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Magic allows for many exciting possibilities during sex, but most of them come with risks. Colette posts a bulletin about safe sex, which receives several responses.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    600
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    7
    + +
    Kudos:
    +
    20
    + +
    Bookmarks:
    +
    3
    + +
    Hits:
    +
    136
    + +
    + + +
  18. +
  19. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    One for One is a card about exchange and sacrifice. The Seal demands a soul.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    6
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    60
    + +
    + + +
  20. +
  21. + +
    + +

    + olin lipu + by + + + + + + + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    28 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The Exodia scene from the 4kids dub, but in toki pona.

    Chapter 1 is the transcript in toki pona and screenshots with captions in sitelen pona. Chapter 2 is transliterations with translation notes. Enjoy!

    +
    + + + + +
    +
    Language:
    +
    toki pona
    +
    Words:
    +
    831
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    12
    + + +
    Hits:
    +
    110
    + +
    + + +
  22. +
  23. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    + A scattered dream that's like a far-off memory. A far-off memory that's like a scattered dream. I want to line the pieces up—yours and mine. +

     

    A collection of cryptic dreams in the mind of a troubled teen.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    7,300
    +
    Chapters:
    +
    4/?
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    2
    + + +
    Hits:
    +
    44
    + +
    + + +
  24. +
  25. + +
    + +

    + Nightmare Horse + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    27 Sep 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The newest Listener meets the oldest horse.

    +

    (Set during "The Cure for Madness.")

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    100
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    3
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    3
    + +
    + + +
  26. +
  27. + +
    + +

    + Mountains + by + + + + + + + + +

    + +
    + Fandoms: + Control (Remedy Video Games) +   +
    + + + +

    27 Sep 2026

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Something is wrong inside the Oldest House. The House knows this.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    150
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    54
    + +
    + + +
  28. +
  29. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Pat tries to help Brian understand a new kind of wrestling.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,670
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    14
    + + +
    Hits:
    +
    184
    + +
    + + +
  30. +
  31. + +
    + +

    + Love Me or Leave Me + by + + + + + + + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    There's a definite pace to Linhardt's strokes now, and he opens his eyes to meet Lysithea's gaze dead-on. "You can leave if you want."

    She can't think of a response to that, not with his piercing eyes on hers, not when she can see those slender fingers moving in such an obscene way. He's right; she could leave as easily as she'd walked in. She just doesn't want to, and Linhardt knows it, the smug bastard.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,595
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    16
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    255
    + +
    + + +
  32. +
  33. + +
    + +

    + Keeping Warm + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls V: Skyrim +   +
    + + + +

    28 Nov 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The College's architecture leaves much to be desired, including doors.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    21
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    186
    + +
    + + +
  34. +
  35. + +
    + +

    + Jaunt + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    07 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Experimental piece: Schrödinger Diary from the perspective of the mouse.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    400
    +
    Chapters:
    +
    1/1
    + + + + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    19
    + +
    + + +
  36. +
  37. + +
    + +

    + Inflict Wounds + by + + + , + + + + + +

    + +
    + Fandoms: + Original Work, Dungeons & Dragons (Roleplaying Game) +   +
    + + + +

    18 May 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Raz'elen tries to talk to Luvo about his feelings. Tribune Nimbus decides to intervene.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    7,689
    +
    Chapters:
    +
    1/1
    + + + + + +
    Hits:
    +
    46
    + +
    + + +
  38. +
  39. + +
    + +

    + Impossible Claims + by + + + + + + + + +

    + +
    + Fandoms: + Elder Scrolls, Elder Scrolls V: Skyrim +   +
    + + + +

    20 Feb 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    "I do not enjoy the cavalier and sarcastic manner with which the author frames his study. While there is valuable and intelligent insight into Dunmer beliefs, the author makes impossible claims about his life as almost marginal comments. See below, where he claims to have had tea with the First Era Dunmer ruler, Nerevar..."

    Mzunchak responds to Calcelmo's review of his latest work.

    (4E 187)

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    755
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    5
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    41
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=word_count/01.html b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=word_count/01.html new file mode 100644 index 00000000..726dbc45 --- /dev/null +++ b/tests/mocks/data/ao3/users/chemicalcain/works/work_search[sort_column]=word_count/01.html @@ -0,0 +1,2857 @@ + + + + + + + + + + + + + + + + chemicalcain - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 1 - 20 of 34 Works by chemicalcain +

+ + + + + + + +

Navigation

+ + +

Listing Works

+
    +
  1. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Raz finally gets to talk to Nimbus alone.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    13,006
    +
    Chapters:
    +
    1/1
    + + + + + +
    Hits:
    +
    44
    + +
    + + +
  2. +
  3. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    After a pretty serious fight in which Raz'elen gets pinned to a wall by a lance through the gut, he and Luvo are unable to sleep and have a bit of a conversation.

    An abridged and edited version of a text RP between cmdonovann and chemicalcain. Raz's parts of the RP are cmdonovann's, and Luvo's parts are chemicalcain's. Editing primarily done by cmdonovann.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    11,443
    +
    Chapters:
    +
    2/2
    + + + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    2
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    82
    + +
    + + +
  4. +
  5. + +
    + +

    + Inflict Wounds + by + + + , + + + + + +

    + +
    + Fandoms: + Original Work, Dungeons & Dragons (Roleplaying Game) +   +
    + + + +

    18 May 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Raz'elen tries to talk to Luvo about his feelings. Tribune Nimbus decides to intervene.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    7,689
    +
    Chapters:
    +
    1/1
    + + + + + +
    Hits:
    +
    46
    + +
    + + +
  6. +
  7. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    + A scattered dream that's like a far-off memory. A far-off memory that's like a scattered dream. I want to line the pieces up—yours and mine. +

     

    A collection of cryptic dreams in the mind of a troubled teen.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    7,300
    +
    Chapters:
    +
    4/?
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    2
    + + +
    Hits:
    +
    44
    + +
    + + +
  8. +
  9. + +
    + +

    + Who Can I Turn To? + by + + + + + + + for cmdonovann + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    21 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    She looks nothing like her older self did when she gave her the notebook and started The Mission. Her hair is cropped short, fluffy and dyed black with a red streak hanging across her eyes, and she's wearing a plaid Hot Topic skirt and fingerless gloves, and she just fixed her eyeliner, and besides, Future Beth must have been at least 25. If Serene is looking for her yet, he's looking for a soldier, not a teenager in fishnets.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    6,952
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    6
    + + +
    Hits:
    +
    100
    + +
    + + +
  10. +
  11. + +
    + +

    + As Sweet As A Song, As Right As A Wrong + by + + + + + + + for hobbitdragon + + +

    + +
    + Fandoms: + Original Work +   +
    + + + +

    11 Aug 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Do you want to be a monster?

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    5,752
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    12
    + +
    Kudos:
    +
    196
    + +
    Bookmarks:
    +
    18
    + +
    Hits:
    +
    4,526
    + +
    + + +
  12. +
  13. + +
    + +

    + Hey Jealousy + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    25 Nov 2018

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    + "if you don't expect too much from me, you might not be let down" +

     

    Paul comes home not long after graduation and receives an unexpected visit from an estranged friend.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    4,777
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    24
    + +
    Bookmarks:
    +
    5
    + +
    Hits:
    +
    542
    + +
    + + +
  14. +
  15. + +
    + +

    + Cravats For Kravitz + by + + + + + + + + +

    + +
    + Fandoms: + The Adventure Zone (Podcast) +   +
    + + + +

    27 Nov 2016

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Taako's drunk and upset. Magnus's obvious solution is to form a band.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,548
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    18
    + +
    Kudos:
    +
    195
    + +
    Bookmarks:
    +
    11
    + +
    Hits:
    +
    1,880
    + +
    + + +
  16. +
  17. + +
    + +

    + Chiaroscuro + by + + + + + + + for VThinksOn + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    20 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    Yugi's limbs move faster than his mind can track. Before he knows it, both of his assailants are on the ground under the heel of studded leather platform boots, which Yugi definitely wasn't wearing when he left the house this morning.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    2,412
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    9
    + +
    Kudos:
    +
    34
    + +
    Bookmarks:
    +
    8
    + +
    Hits:
    +
    312
    + +
    + + +
  18. +
  19. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Co-occurring with Nothing to Say, but from Luvo's point of view. I strongly recommend reading Nothing to Say first, because the framing and author's notes will make the context of this scene much clearer.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,797
    +
    Chapters:
    +
    1/1
    + + + + +
    Kudos:
    +
    1
    + + +
    Hits:
    +
    15
    + +
    + + +
  20. +
  21. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Pat tries to help Brian understand a new kind of wrestling.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,670
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    10
    + +
    Kudos:
    +
    14
    + + +
    Hits:
    +
    184
    + +
    + + +
  22. +
  23. + +
    + +

    + Love Me or Leave Me + by + + + + + + + + +

    + +
    + Fandoms: + Fire Emblem: Fuukasetsugetsu | Fire Emblem: Three Houses +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    There's a definite pace to Linhardt's strokes now, and he opens his eyes to meet Lysithea's gaze dead-on. "You can leave if you want."

    She can't think of a response to that, not with his piercing eyes on hers, not when she can see those slender fingers moving in such an obscene way. He's right; she could leave as easily as she'd walked in. She just doesn't want to, and Linhardt knows it, the smug bastard.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,595
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    16
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    255
    + +
    + + +
  24. +
  25. + +
    + +

    + Control + by + + + + + + + + +

    + +
    + Fandoms: + Original Work, Dungeons & Dragons (Roleplaying Game) +   +
    + + + +

    25 Feb 2024

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    That distraction, that distance from his body, is why Luvo doesn't notice the cup on the ground in time. It crunches under his foot; he gives away their position and the target runs. The moment he recognizes his own failure is the moment the fury within him takes over. Luvo feels his grasp on the body slip completely as the transformation takes hold. All he can do is watch. Tribune Nimbus obliterates a Hell Knight in a single strike, and Luvo realizes he no longer has any power over his actions.

    ---

    Or, the first time Luvo fully dissociates from the body.

    +
    + +
    Series
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,345
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    1
    + + +
    Hits:
    +
    16
    + +
    + + +
  26. +
  27. + +
    + +

    + Curse-of-Dreams + by + + + + + + + for Allekha + + +

    + +
    + Fandoms: + Elder Scrolls III: Morrowind +   +
    + + + +

    08 Jun 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    When an Ashlander contracts Corprus and is exiled from the tribe, servants of the Sixth House seize the opportunity to increase their ranks. The new recruit begins to have recurring dreams of the Nerevarine... or several Nerevarines.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,231
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    4
    + +
    Kudos:
    +
    9
    + + +
    Hits:
    +
    28
    + +
    + + +
  28. +
  29. + +
    + +

    + Chosen + by + + + + + + + + +

    + +
    + Fandoms: + The Silt Verses (Podcast) +   +
    + + + +

    02 Apr 2025

    +
    + + +
    Tags
    + + + + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,164
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    14
    + +
    Bookmarks:
    +
    2
    + +
    Hits:
    +
    78
    + +
    + + +
  30. +
  31. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Something goes wrong with Lysithea during a battle. Hanneman suggests a medical procedure to investigate. It goes badly.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,135
    +
    Chapters:
    +
    1/1
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    3
    + + +
    Hits:
    +
    48
    + +
    + + +
  32. +
  33. + + + + +
    Tags
    + + + +
    Summary
    +
    +

    Atlantis is back, and they're really into full contact nude sports. The world is wild about it.

    +

    Fuckathlon is an open AU setting for fanfiction or original works. Chapter 1 will detail the core premise, and chapter 2 will pose questions for you to dig into the setting and figure out how to put your favorite weirdos in it.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,119
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    1
    + +
    Kudos:
    +
    9
    + +
    Bookmarks:
    +
    4
    + +
    Hits:
    +
    175
    + +
    + + +
  34. +
  35. + +
    + +

    + Thirty Clove + by + + + + + + + + +

    + +
    + Fandoms: + The Adventure Zone (Podcast) +   +
    + + + +

    08 Nov 2016

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    (Post Eleventh Hour, so, spoilers.) Taako's dealing with what he learned in Refuge, and he's reliving it all over again.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    1,068
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    5
    + +
    Kudos:
    +
    149
    + +
    Bookmarks:
    +
    9
    + +
    Hits:
    +
    1,211
    + +
    + + +
  36. +
  37. + +
    + +

    + Go to bed + by + + + + + + + + +

    + +
    + Fandoms: + Quantum Break (Video Game) +   +
    + + + +

    17 Mar 2017

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    When Jack gets home from a late shift, Paul's still up.

    +
    + + + + +
    +
    Language:
    +
    English
    +
    Words:
    +
    937
    +
    Chapters:
    +
    1/1
    + + + +
    Comments:
    +
    3
    + +
    Kudos:
    +
    27
    + +
    Bookmarks:
    +
    1
    + +
    Hits:
    +
    382
    + +
    + + +
  38. +
  39. + +
    + +

    + olin lipu + by + + + + + + + + +

    + +
    + Fandoms: + Yu-Gi-Oh! Duel Monsters (Anime & Manga) +   +
    + + + +

    28 May 2025

    +
    + + +
    Tags
    + + + +
    Summary
    +
    +

    The Exodia scene from the 4kids dub, but in toki pona.

    Chapter 1 is the transcript in toki pona and screenshots with captions in sitelen pona. Chapter 2 is transliterations with translation notes. Enjoy!

    +
    + + + + +
    +
    Language:
    +
    toki pona
    +
    Words:
    +
    831
    +
    Chapters:
    +
    2/2
    + + +
    Collections:
    +
    1
    + +
    Comments:
    +
    2
    + +
    Kudos:
    +
    12
    + + +
    Hits:
    +
    110
    + +
    + + +
  40. + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + +

Navigation

+ +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/data/ao3/users/franzeska/works.html b/tests/mocks/data/ao3/users/franzeska/works.html new file mode 100644 index 00000000..31942c5c --- /dev/null +++ b/tests/mocks/data/ao3/users/franzeska/works.html @@ -0,0 +1,793 @@ + + + + + + + + + + + + + + + + + + Franzeska - Works | Archive of Our Own + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ +
+

Choices

+ + + +

Pitch

+ + + +

Switch

+ + +
+ + + + +
+ +
+ + + +

+ 0 Works by Franzeska +

+ + + + + + + + + + +

Listing Works

+
    + +
+ + + + +
+

Filters

+
Filter results: +
+
Submit
+
+
+ +
+
+ +
+ +
+

+ Include +

+ ? +
+
+
+
+ Include Ratings +
+ +
+ Include Warnings +
+ +
+ Include Categories +
+ +
+ Include Fandoms +
+ +
+ Include Characters +
+ +
+ Include Relationships +
+ +
+ Include Additional Tags +
+ + + +
+
+
+

+ Exclude +

+ ? +
+
+
+
+ Exclude Ratings +
+ +
+ Exclude Warnings +
+ +
+ Exclude Categories +
+ +
+ Exclude Fandoms +
+ +
+ Exclude Characters +
+ +
+ Exclude Relationships +
+ +
+ Exclude Additional Tags +
+ + + +
+
+ +
+

More Options

+
+ +
+
+
Crossovers
+ +
Completion Status
+ +
Word Count
+ +
Date Updated
+ + + + + +
+ +
+
+ +
+
+
+
Submit
+
+
+ +

+ Clear Filters +

+ +
+ + + + + + +
+ +
+ + +
+ + + + + +
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/mocks/handlers.ts b/tests/mocks/handlers.ts index 1798ff90..f1c157d5 100644 --- a/tests/mocks/handlers.ts +++ b/tests/mocks/handlers.ts @@ -3,6 +3,7 @@ import feedHandlers from "./handlers/tags/feed"; import nameHandlers from "./handlers/tags/name"; import searchHandlers from "./handlers/tags/search"; import profileHandlers from "./handlers/users/profile"; +import userWorksHandlers from "./handlers/users/works"; import seriesHandlers from "./handlers/series"; import tagWorksHandlers from "./handlers/tags/works"; import workPageHandlers from "./handlers/works"; @@ -14,6 +15,7 @@ import { HttpHandler } from "msw"; export default [ profileHandlers, + userWorksHandlers, feedHandlers, tagWorksHandlers, searchHandlers, diff --git a/tests/mocks/handlers/users/works.ts b/tests/mocks/handlers/users/works.ts new file mode 100644 index 00000000..86d6580f --- /dev/null +++ b/tests/mocks/handlers/users/works.ts @@ -0,0 +1,19 @@ +import fs from "fs"; +import { http, HttpHandler, HttpResponse } from "msw"; +import { getFilePathFromUrl } from "../../scripts/utils.mjs"; + +const respondWithWorksFile = ({ request }: { request: Request }) => { + const html = fs.readFileSync(getFilePathFromUrl(request.url)); + + return new HttpResponse(html, { + headers: { "Content-Type": "text/html" }, + }); +}; + +export default [ + http.all("https://archiveofourown.org/users/:name/works", respondWithWorksFile), + http.all( + "https://archiveofourown.org/users/:name/pseuds/:pseud/works", + respondWithWorksFile + ), +] satisfies HttpHandler[]; diff --git a/tests/mocks/scripts/utils.mts b/tests/mocks/scripts/utils.mts index 7b476be6..7d8fffab 100644 --- a/tests/mocks/scripts/utils.mts +++ b/tests/mocks/scripts/utils.mts @@ -108,7 +108,7 @@ export function getArchiveDataDir(archive: "ao3" | "superlove" = "ao3") { return path.join(getRootDataDir(), archive); } -const getNormalizedTagSearchFolder = (searchParams: URLSearchParams) => { +const getNormalizedQueryFolder = (searchParams: URLSearchParams) => { const entries: string[] = []; // Sort the parameters to make the folder name deterministic const keys = Array.from(new Set([...searchParams.keys()])).sort(); @@ -136,16 +136,36 @@ const getNormalizedTagSearchFolder = (searchParams: URLSearchParams) => { return entries.join("__"); }; +const getPageFileName = (searchParams: URLSearchParams) => { + const page = Number.parseInt(searchParams.get("page") ?? "1", 10); + return `${String(page).padStart(2, "0")}.html`; +}; + export const getFilePathForSearchUrl = (parsedUrl: URL) => { - const folderName = getNormalizedTagSearchFolder(parsedUrl.searchParams); - const rawPage = parsedUrl.searchParams.get("page"); - const page = Number.parseInt(rawPage ?? "1", 10); - const fileName = `${String(page).padStart(2, "0")}.html`; + const folderName = getNormalizedQueryFolder(parsedUrl.searchParams); + const fileName = getPageFileName(parsedUrl.searchParams); // TODO: make this support other search types with time return path.join("tag-search", folderName, fileName); }; +// Sorted or paginated user works listings get one folder per query, so they +// don't overwrite the default works.html. Returns null for the default listing, +// which is stored as works.html like any other works page. +const getUserWorksQueryPath = (parsedUrl: URL) => { + const searchParams = new URLSearchParams(parsedUrl.searchParams); + // Sorting by revised_at is what AO3 does when no sort is given. + if (searchParams.get("work_search[sort_column]") === "revised_at") { + searchParams.delete("work_search[sort_column]"); + } + const folderName = getNormalizedQueryFolder(searchParams); + const fileName = getPageFileName(searchParams); + if (folderName === "default" && fileName === "01.html") { + return null; + } + return path.join(folderName, fileName); +}; + export function getFilePathFromUrl(url: string | URL) { const archive = getArchiveFromUrl(url); const parsedUrl = new URL(url); @@ -163,6 +183,19 @@ export function getFilePathFromUrl(url: string | URL) { ); } + if (segments[0] === "users" && lastSegment === "works") { + const queryPath = getUserWorksQueryPath(parsedUrl); + if (queryPath) { + return path.join( + getArchiveDataDir(archive), + ...segments.map((segment) => + safeFilenamify(decodeURIComponent(segment)), + ), + queryPath, + ); + } + } + // If the last segment is a file, use segments up to the last one for the directory // Otherwise use all segments, unless the last path is "works". const dirSegments = diff --git a/tests/urls.test.ts b/tests/urls.test.ts index 9e3eb216..a19a13f5 100644 --- a/tests/urls.test.ts +++ b/tests/urls.test.ts @@ -1,8 +1,11 @@ import { describe, it, expect } from "vitest"; import { + getAuthorFromUrl, getSearchUrlFromTagFilters, getTagUrl, getTagWorksFeedUrl, + getUserDetailsFromUrl, + getUserWorksUrl, } from "src/urls"; /** @@ -389,3 +392,140 @@ describe("getTagWorksFeedUrl", () => { expect(url.pathname).toBe("/tags/Tom%20*a*%20Jerry/works"); }); }); + +describe("getUserWorksUrl", () => { + it("should build the works URL for a user", () => { + const url = new URL(getUserWorksUrl({ username: "astolat" })); + + expect(url.pathname).toBe("/users/astolat/works"); + expect(url.search).toBe(""); + }); + + it("should build the works URL for a pseud of a user", () => { + const url = new URL( + getUserWorksUrl({ username: "astolat", pseud: "the lady of shalott" }) + ); + + expect(url.pathname).toBe( + "/users/astolat/pseuds/the%20lady%20of%20shalott/works" + ); + }); + + it("should include the page number", () => { + const url = new URL(getUserWorksUrl({ username: "astolat", page: 2 })); + + expect(url.searchParams.get("page")).toBe("2"); + }); + + it("should wrap sort parameters in work_search[]", () => { + const url = new URL( + getUserWorksUrl({ + username: "astolat", + sortColumn: "kudos_count", + sortDirection: "asc", + }) + ); + + expect(url.searchParams.get("work_search[sort_column]")).toBe( + "kudos_count" + ); + expect(url.searchParams.get("work_search[sort_direction]")).toBe("asc"); + }); + + it.each([ + ["authors", "authors_to_sort_on"], + ["title", "title_to_sort_on"], + ["updated_at", "revised_at"], + ["created_at", "created_at"], + ] as const)("should send sort column %s as %s", (sortColumn, ao3Column) => { + const url = new URL(getUserWorksUrl({ username: "astolat", sortColumn })); + + expect(url.searchParams.get("work_search[sort_column]")).toBe(ao3Column); + }); + + it("should omit sort parameters that are not provided", () => { + const url = new URL( + getUserWorksUrl({ username: "astolat", sortDirection: "desc" }) + ); + + expect(url.searchParams.get("work_search[sort_direction]")).toBe("desc"); + expect(url.searchParams.has("work_search[sort_column]")).toBe(false); + }); + + it("should encode special characters in the username", () => { + const url = new URL(getUserWorksUrl({ username: "a user" })); + + expect(url.pathname).toBe("/users/a%20user/works"); + }); +}); + +describe("getAuthorFromUrl", () => { + it("should read the username and pseud from a pseud URL", () => { + expect(getAuthorFromUrl({ url: "/users/astolat/pseuds/astolat" })).toEqual({ + username: "astolat", + pseud: "astolat", + anonymous: false, + }); + }); + + it("should decode pseuds with special characters", () => { + expect( + getAuthorFromUrl({ url: "/users/franzeska/pseuds/Franzeska%20Ewart" }), + ).toEqual({ + username: "franzeska", + pseud: "Franzeska Ewart", + anonymous: false, + }); + }); + + it("should throw for author URLs without a pseud", () => { + expect(() => getAuthorFromUrl({ url: "/users/astolat" })).toThrow( + "Unexpected author URL: /users/astolat", + ); + }); + + it("should throw for URLs that don't point to a user", () => { + expect(() => getAuthorFromUrl({ url: "/works/12345" })).toThrow( + "Invalid user URL: /works/12345", + ); + }); +}); + +describe("getUserDetailsFromUrl", () => { + it.each([ + "/users/astolat", + "/users/astolat/", + "/users/astolat/profile", + "/users/astolat/works", + "/users/astolat/works?page=2", + "https://archiveofourown.org/users/astolat", + "https://archiveofourown.org/users/astolat/works?work_search%5Bsort_column%5D=hits", + ])("should read only the username from %s", (url) => { + expect(getUserDetailsFromUrl({ url })).toEqual({ username: "astolat" }); + }); + + it.each([ + "/users/astolat/pseuds/astolat", + "/users/astolat/pseuds/astolat/works", + "https://archiveofourown.org/users/astolat/pseuds/astolat/works?page=2", + ])("should read the username and pseud from %s", (url) => { + expect(getUserDetailsFromUrl({ url })).toEqual({ + username: "astolat", + pseud: "astolat", + }); + }); + + it("should round-trip the URLs we build for user works", () => { + expect( + getUserDetailsFromUrl({ + url: getUserWorksUrl({ username: "franzeska", pseud: "Franzeska Ewart" }), + }), + ).toEqual({ username: "franzeska", pseud: "Franzeska Ewart" }); + }); + + it("should throw for URLs that don't point to a user", () => { + expect(() => getUserDetailsFromUrl({ url: "/works/12345" })).toThrow( + "Invalid user URL: /works/12345", + ); + }); +}); diff --git a/tests/user-works.test.ts b/tests/user-works.test.ts new file mode 100644 index 00000000..2f4dc7b3 --- /dev/null +++ b/tests/user-works.test.ts @@ -0,0 +1,384 @@ +import { getUserWorks } from "src/index"; +import { ArchivePageRequestError } from "src/utils"; +import { describe, it, expect } from "vitest"; +import { http, HttpResponse } from "msw"; +import server from "./mocks/server"; + +describe("User/works", () => { + it("should fetch the first page of a user's works", async () => { + const result = await getUserWorks({ username: "astolat" }); + + expect(result.works).toHaveLength(20); + expect(result.pages).toEqual({ + current: 1, + total: expect.driftingCount({ atLeast: 27, atMost: 40 }), + }); + expect(result.totalResults).driftingCount({ atLeast: 535, atMost: 800 }); + + // We just look at the titles to make the snapshot more readable + expect(result.works.map((work) => work.title)).toMatchInlineSnapshot(` + [ + "Research", + "Inheritance", + "Looking At Stone", + "Hallelujah", + "Tourney Field", + "Oubliette", + "Entrapment", + "Press Gang", + "Common Knowledge", + "The Pack Survives", + "The Price of Bread and Salt", + "A Man of Honor", + "Let The River Run", + "Winter's Crown", + "Royal Flush", + "Lifeline", + "Heal Thyself", + "Raised By Wolves", + "Paying Debts", + "The Next Time", + ] + `); + }); + + it("should fetch the details of a work in the list", async () => { + const result = await getUserWorks({ username: "astolat" }); + + // Pick an older, finished work by id so new uploads don't shift the snapshot + const work = result.works.find((work) => work.id === 43703871); + + expect(work).toMatchInlineSnapshot(` + { + "adult": false, + "authors": [ + { + "anonymous": false, + "pseud": "astolat", + "username": "astolat", + }, + ], + "chapters": { + "published": 9, + "total": 9, + }, + "complete": true, + "fandoms": [ + "Game of Thrones (TV)", + ], + "id": 43703871, + "language": "English", + "shortUrl": "https://ao3.org/works/43703871", + "stats": { + "bookmarks": 1255, + "hits": 111695, + "kudos": 5685, + }, + "summary": "

The deep satisfaction of having made the right choice; of having found a clear-flowing wellspring of true honor to protect.

", + "tags": { + "additional": [ + "Red Wedding", + "Riverlands", + "Grief", + "Recovery", + "Suicidal Thoughts", + ], + "characters": [ + "Brienne of Tarth", + "Robb Stark", + "Jaime Lannister", + "Tyrion Lannister", + "Sansa Stark", + "Arya Stark", + "Catelyn Tully Stark", + "Daenerys Targaryen", + ], + "relationships": [ + "Jaime Lannister/Robb Stark/Brienne of Tarth", + "Jaime Lannister/Brienne of Tarth", + "Jaime Lannister/Robb Stark", + "Tyrion Lannister & Sansa Stark", + "pre-Tyrion Lannister/Sansa Stark", + "Robb Stark & Brienne of Tarth", + "Jaime Lannister & Tyrion Lannister", + "Arya Stark & Robb Stark", + "Robb Stark/Daenerys Targaryen", + ], + }, + "title": "Let The River Run", + "updatedAt": "2022-12-25", + "url": "https://archiveofourown.org/works/43703871", + "words": 61498, + } + `); + }); + + it("should return the filters that were requested", async () => { + const result = await getUserWorks({ + username: "chemicalcain", + sortColumn: "title", + sortDirection: "desc", + }); + + expect(result.filters).toEqual({ + sortColumn: "title", + sortDirection: "desc", + page: 1, + }); + // AO3 sorts on a normalized title: lowercased, leading "a"/"an"/"the" moved + // to the end, so "The Mega-Blast..." sorts under "m". See + // https://github.com/otwcode/otwarchive/blob/9dd99008f52ccc726978db131181421f4eb38fe5/app/models/work.rb#L1106-L1112 + expect(result.works.map((work) => work.title)).toMatchInlineSnapshot(` + [ + "Who Can I Turn To?", + "What do you want?", + "Visceral", + "Trust", + "Toy Soldier", + "Thirty Clove", + "Something to focus on...", + "Self-Reflection", + "Reckless Recreational Spell Use", + "One for One", + "olin lipu", + "Nothing is Whole and Nothing is Broken", + "Nightmare Horse", + "Mountains", + "The Mega-Blast Dick Breaker", + "Love Me or Leave Me", + "Keeping Warm", + "Jaunt", + "Inflict Wounds", + "Impossible Claims", + ] + `); + }); + + it("should fetch an empty list for a user with no works", async () => { + const result = await getUserWorks({ username: "franzeska" }); + + expect(result).toEqual({ + filters: { sortColumn: "updated_at", sortDirection: "desc", page: 1 }, + works: [], + pages: { total: 0, current: 1 }, + totalResults: 0, + }); + }); + + it("should throw ArchivePageRequestError for a user that doesn't exist", async () => { + server.use( + http.get( + "https://archiveofourown.org/users/missinguser/works", + () => new HttpResponse(null, { status: 404 }), + ), + ); + const failedWorks = getUserWorks({ username: "missinguser" }); + + await expect(failedWorks).rejects.toThrow(ArchivePageRequestError); + await expect(failedWorks).rejects.toThrow( + "Archive request failed with status 404: https://archiveofourown.org/users/missinguser/works?page=1&work_search%5Bsort_column%5D=revised_at", + ); + }); + + it("should fetch a user's works sorted by most recently updated", async () => { + const result = await getUserWorks({ username: "chemicalcain" }); + + expect(result.filters).toEqual({ + sortColumn: "updated_at", + sortDirection: "desc", + page: 1, + }); + expect(result.works).toHaveLength(20); + expect(result.works.slice(0, 5).map((work) => work.title)) + .toMatchInlineSnapshot(` + [ + "Mountains", + "Nightmare Horse", + "Toy Soldier", + "One for One", + "Blasphemy", + ] + `); + }); + + it("should fetch a user's works sorted by hits", async () => { + const result = await getUserWorks({ + username: "chemicalcain", + sortColumn: "hits", + }); + + expect(result.filters).toEqual({ + sortColumn: "hits", + sortDirection: "desc", + page: 1, + }); + expect(result.works).toHaveLength(20); + expect(result.works.slice(0, 5).map((work) => work.title)) + .toMatchInlineSnapshot(` + [ + "As Sweet As A Song, As Right As A Wrong", + "Cravats For Kravitz", + "Self-Reflection", + "Thirty Clove", + "Hey Jealousy", + ] + `); + }); + + it("should fetch only the works posted under a pseud", async () => { + const result = await getUserWorks({ + username: "astolat", + pseud: "shalott", + }); + + expect(result.works.length).toBeGreaterThan(0); + for (const work of result.works) { + expect(work.authors).toContainEqual({ + username: "astolat", + pseud: "shalott", + anonymous: false, + }); + } + expect(result.totalResults).toMatchInlineSnapshot(`1`); + }); + + it("should fetch the works of a pseud with spaces in its name", async () => { + const result = await getUserWorks({ + username: "astolat", + pseud: "the lady of shalott", + }); + + expect(result.works).toHaveLength(18); + expect(result.pages).toEqual({ total: 1, current: 1 }); + expect(result.totalResults).toBe(18); + for (const work of result.works) { + expect(work.authors).toEqual([ + { username: "astolat", pseud: "the lady of shalott", anonymous: false }, + ]); + } + }); + + it("should fetch the first page of a pseud's works across many pages", async () => { + const result = await getUserWorks({ username: "astolat", pseud: "astolat" }); + + expect(result.works).toHaveLength(20); + expect(result.pages).toEqual({ + current: 1, + total: expect.driftingCount({ atLeast: 26, atMost: 40 }), + }); + expect(result.totalResults).driftingCount({ atLeast: 516, atMost: 800 }); + for (const work of result.works) { + expect(work.authors).toContainEqual({ + username: "astolat", + pseud: "astolat", + anonymous: false, + }); + } + }); + + it("should fetch a pseud's works sorted by hits", async () => { + const result = await getUserWorks({ + username: "chemicalcain", + pseud: "chemicalcain", + sortColumn: "hits", + }); + + expect(result.filters).toEqual({ + sortColumn: "hits", + sortDirection: "desc", + page: 1, + }); + expect(result.works).toHaveLength(20); + expect(result.pages).toEqual({ total: 2, current: 1 }); + expect(result.totalResults).toBe(34); + expect(result.works.slice(0, 5).map((work) => work.title)) + .toMatchInlineSnapshot(` + [ + "As Sweet As A Song, As Right As A Wrong", + "Cravats For Kravitz", + "Self-Reflection", + "Thirty Clove", + "Hey Jealousy", + ] + `); + }); + + it("should list the same works for a user's only pseud", async () => { + const userWorks = await getUserWorks({ username: "chemicalcain" }); + const pseudWorks = await getUserWorks({ + username: "chemicalcain", + pseud: "chemicalcain", + }); + + expect(pseudWorks.works).toEqual(userWorks.works); + expect(pseudWorks.totalResults).toBe(userWorks.totalResults); + }); + + it("should fetch a user's works sorted by title", async () => { + const result = await getUserWorks({ + username: "chemicalcain", + sortColumn: "title", + }); + + // AO3 sorts titles A to Z when no direction is given. + expect(result.filters).toEqual({ + sortColumn: "title", + sortDirection: "asc", + page: 1, + }); + expect(result.works).toHaveLength(20); + // AO3 sorts on a normalized title: lowercased, leading "a"/"an"/"the" moved + // to the end, so "The Mega-Blast..." sorts under "m". See + // https://github.com/otwcode/otwarchive/blob/9dd99008f52ccc726978db131181421f4eb38fe5/app/models/work.rb#L1106-L1112 + expect(result.works.slice(0, 5).map((work) => work.title)) + .toMatchInlineSnapshot(` + [ + "Armadillidiidae", + "As Sweet As A Song, As Right As A Wrong", + "Beauty", + "Blasphemy", + "Bold", + ] + `); + }); + + it("should fetch a user's works sorted by date posted", async () => { + const result = await getUserWorks({ + username: "chemicalcain", + sortColumn: "created_at", + }); + + expect(result.filters.sortColumn).toBe("created_at"); + expect(result.works).toHaveLength(20); + expect(result.works.slice(0, 5).map((work) => work.title)) + .toMatchInlineSnapshot(` + [ + "Mountains", + "Nightmare Horse", + "Toy Soldier", + "One for One", + "Blasphemy", + ] + `); + }); + + it("should fetch a user's works sorted by word count", async () => { + const result = await getUserWorks({ + username: "chemicalcain", + sortColumn: "word_count", + }); + + expect(result.filters.sortColumn).toBe("word_count"); + expect(result.works).toHaveLength(20); + expect(result.works.slice(0, 5).map((work) => work.title)) + .toMatchInlineSnapshot(` + [ + "What do you want?", + "Something to focus on...", + "Inflict Wounds", + "Nothing is Whole and Nothing is Broken", + "Who Can I Turn To?", + ] + `); + }); +}); diff --git a/tests/works.test.ts b/tests/works.test.ts index ec05ce3d..d10886ab 100644 --- a/tests/works.test.ts +++ b/tests/works.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from "vitest"; +import { http, HttpResponse } from "msw"; +import server from "./mocks/server"; import { getWorkDetailsFromUrl, getWorkUrl } from "src/urls"; -import { InvalidIDError } from "src/utils"; +import { ArchivePageRequestError, InvalidIDError } from "src/utils"; import type { WorkSummary } from "types/entities"; import { getWork } from "src/index"; @@ -85,6 +87,21 @@ describe("Works/data", () => { ); }); + it("should throw ArchivePageRequestError when the archive returns an error", async () => { + server.use( + http.get( + "https://archiveofourown.org/*", + () => new HttpResponse(null, { status: 404 }), + ), + ); + const failedWork = getWork({ workId: 1 }); + + await expect(failedWork).rejects.toThrow(ArchivePageRequestError); + await expect(failedWork).rejects.toThrow( + "Archive request failed with status 404: https://archiveofourown.org/works/1", + ); + }); + it("should fetch work information in its entirety", async () => { const work = await getWork({ workId: 29046888, diff --git a/types/entities.ts b/types/entities.ts index 75b7c9e6..036c52b3 100644 --- a/types/entities.ts +++ b/types/entities.ts @@ -90,7 +90,33 @@ export interface User { bioHtml: string | null; } -export interface SeriesWorkSummary +export interface UserWorksFilters { + sortColumn: + | "authors" + | "title" + | "created_at" + | "updated_at" + | "word_count" + | "hits" + | "kudos_count" + | "comments_count" + | "bookmarks_count"; + sortDirection: "asc" | "desc"; + page: number; +} + +export interface UserWorks { + filters: UserWorksFilters; + totalResults: number; + pages: { + total: number; + current: number; + }; + works: WorkBlurbSummary[]; +} + +// A work as AO3 shows it in listings like series and user works pages. +export interface WorkBlurbSummary extends Omit< WorkSummary, | "category" @@ -108,6 +134,9 @@ export interface SeriesWorkSummary stats: Omit; } +/** @deprecated Use `WorkBlurbSummary` instead. */ +export type SeriesWorkSummary = WorkBlurbSummary; + export interface Series { id: ArchiveId; name: string; @@ -120,7 +149,7 @@ export interface Series { bookmarks: number; complete: boolean; workCount: number; - works: SeriesWorkSummary[]; + works: WorkBlurbSummary[]; } export enum WorkRatings { diff --git a/types/zod/generated/schemas.ts b/types/zod/generated/schemas.ts index 03e33a65..08c4d553 100644 --- a/types/zod/generated/schemas.ts +++ b/types/zod/generated/schemas.ts @@ -21,7 +21,8 @@ export const tagSchema = z.object({ subTags: z.array(z.object({ tagName: z.string(), parentSubTag: z.string().nullable() - })) + })), + tagsWithSameMeaning: z.array(z.string()) }); export const tagSearchTypeSchema = z.union([z.literal("fandom"), z.literal("character"), z.literal("relationship"), z.literal("freeform"), z.literal("any")]); @@ -72,6 +73,12 @@ export const userSchema = z.object({ bioHtml: z.string().nullable() }); +export const userWorksFiltersSchema = z.object({ + sortColumn: z.union([z.literal("authors"), z.literal("title"), z.literal("created_at"), z.literal("updated_at"), z.literal("word_count"), z.literal("hits"), z.literal("kudos_count"), z.literal("comments_count"), z.literal("bookmarks_count")]), + sortDirection: z.union([z.literal("asc"), z.literal("desc")]), + page: z.number() +}); + export const workRatingsSchema = z.nativeEnum(WorkRatings); export const workCategorySchema = z.nativeEnum(WorkCategory); @@ -154,13 +161,15 @@ export const chapterSchema = z.object({ shortUrl: z.string() }); -export const seriesWorkSummarySchema = workSummarySchema.omit({ "category": true, "publishedAt": true, "rating": true, "tags": true, "stats": true, "locked": true, "chapterInfo": true, "series": true }).extend({ +export const workBlurbSummarySchema = workSummarySchema.omit({ "category": true, "publishedAt": true, "rating": true, "tags": true, "stats": true, "locked": true, "chapterInfo": true, "series": true }).extend({ url: z.string(), shortUrl: z.string(), tags: workSummarySchema.shape.tags.omit({ "warnings": true }), stats: workSummarySchema.shape.stats.omit({ "comments": true }) }); +export const seriesWorkSummarySchema = workBlurbSummarySchema; + export const seriesSchema = z.object({ id: archiveIdSchema, name: z.string(), @@ -173,5 +182,15 @@ export const seriesSchema = z.object({ bookmarks: z.number(), complete: z.boolean(), workCount: z.number(), - works: z.array(seriesWorkSummarySchema) + works: z.array(workBlurbSummarySchema) +}); + +export const userWorksSchema = z.object({ + filters: userWorksFiltersSchema, + totalResults: z.number(), + pages: z.object({ + total: z.number(), + current: z.number() + }), + works: z.array(workBlurbSummarySchema) }); diff --git a/vitest.config.ts b/vitest.config.ts index 1317622d..bb965fb6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vitest/config'; +import tsconfigPaths from 'vite-tsconfig-paths'; import FailedUrlsReporter from './tests/failed-urls-reporter'; export default defineConfig({ + plugins: [tsconfigPaths()], test: { setupFiles: './tests/setup.ts', reporters: ['default', new FailedUrlsReporter()]