generated from tpl/obsidian-sample-plugin
Move goodreads functions to class to expose via the plugin api
This commit is contained in:
parent
c58f1c4bf5
commit
ad990dae8a
|
@ -1,4 +1,4 @@
|
||||||
import { getBookByLegacyId, type SearchResult } from "@data-sources/goodreads";
|
import type { Goodreads, SearchResult } from "@data-sources/Goodreads";
|
||||||
import { GoodreadsSearchModal, GoodreadsSearchSuggestModal } from "@ui/modals";
|
import { GoodreadsSearchModal, GoodreadsSearchSuggestModal } from "@ui/modals";
|
||||||
import { App, Notice } from "obsidian";
|
import { App, Notice } from "obsidian";
|
||||||
import { Command } from "./Command";
|
import { Command } from "./Command";
|
||||||
|
@ -7,6 +7,7 @@ import type { Book } from "@src/types";
|
||||||
export class SearchGoodreadsCommand extends Command {
|
export class SearchGoodreadsCommand extends Command {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly app: App,
|
private readonly app: App,
|
||||||
|
private readonly goodreads: Goodreads,
|
||||||
private readonly cb: (book: Book) => void
|
private readonly cb: (book: Book) => void
|
||||||
) {
|
) {
|
||||||
super("search-goodreads", "Search Goodreads");
|
super("search-goodreads", "Search Goodreads");
|
||||||
|
@ -15,7 +16,10 @@ export class SearchGoodreadsCommand extends Command {
|
||||||
async callback() {
|
async callback() {
|
||||||
let results: SearchResult[];
|
let results: SearchResult[];
|
||||||
try {
|
try {
|
||||||
results = await GoodreadsSearchModal.createAndOpen(this.app);
|
results = await GoodreadsSearchModal.createAndOpen(
|
||||||
|
this.app,
|
||||||
|
this.goodreads
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to search Goodreads:", error);
|
console.error("Failed to search Goodreads:", error);
|
||||||
new Notice(
|
new Notice(
|
||||||
|
@ -35,7 +39,9 @@ export class SearchGoodreadsCommand extends Command {
|
||||||
|
|
||||||
let book: Book;
|
let book: Book;
|
||||||
try {
|
try {
|
||||||
book = await getBookByLegacyId(selectedResult.legacyId);
|
book = await this.goodreads.getBookByLegacyId(
|
||||||
|
selectedResult.legacyId
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get book:", error);
|
console.error("Failed to get book:", error);
|
||||||
new Notice("Failed to get book. Check console for details.");
|
new Notice("Failed to get book. Check console for details.");
|
||||||
|
|
|
@ -0,0 +1,272 @@
|
||||||
|
import { requestUrl } from "obsidian";
|
||||||
|
import type {
|
||||||
|
Author,
|
||||||
|
Book as OutputBook,
|
||||||
|
Series as OutputSeries,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
interface Ref {
|
||||||
|
__ref: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookContributorEdge {
|
||||||
|
__typename: "BookContributorEdge";
|
||||||
|
node: Ref;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookSeries {
|
||||||
|
__typename: "BookSeries";
|
||||||
|
userPosition: string;
|
||||||
|
series: Ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Genre {
|
||||||
|
__typename: "Genre";
|
||||||
|
name: string;
|
||||||
|
webUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookGenre {
|
||||||
|
__typename: "BookGenre";
|
||||||
|
genre: Genre;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Language {
|
||||||
|
__typename: "Language";
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookDetails {
|
||||||
|
__typename: "BookDetails";
|
||||||
|
asin: string;
|
||||||
|
format: string;
|
||||||
|
numPages: number;
|
||||||
|
publicationTime: number;
|
||||||
|
publisher: string;
|
||||||
|
isbn: string;
|
||||||
|
isbn13: string;
|
||||||
|
language: Language;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Book {
|
||||||
|
__typename: "Book";
|
||||||
|
id: string;
|
||||||
|
legacyId: number;
|
||||||
|
webUrl: string;
|
||||||
|
title: string;
|
||||||
|
titleComplete: string;
|
||||||
|
description: string;
|
||||||
|
primaryContributorEdge: BookContributorEdge;
|
||||||
|
secondaryContributorEdges: BookContributorEdge[];
|
||||||
|
imageUrl: string;
|
||||||
|
bookSeries: BookSeries[];
|
||||||
|
bookGenres: BookGenre[];
|
||||||
|
details: BookDetails;
|
||||||
|
work: Ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContributorWorksConnection {
|
||||||
|
__typename: "ContributorWorksConnection";
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContributorFollowersConnection {
|
||||||
|
__typename: "ContributorFollowersConnection";
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Contributor {
|
||||||
|
__typename: "Contributor";
|
||||||
|
id: string;
|
||||||
|
legacyId: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isGrAuthor: boolean;
|
||||||
|
works: ContributorWorksConnection;
|
||||||
|
profileImageUrl: string;
|
||||||
|
webUrl: string;
|
||||||
|
followers: ContributorFollowersConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Series {
|
||||||
|
__typename: "Series";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
webUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Work {
|
||||||
|
__typename: "Work";
|
||||||
|
id: string;
|
||||||
|
legacyId: number;
|
||||||
|
bestBook: Ref;
|
||||||
|
// ...Book
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Query {
|
||||||
|
__typename: "Query";
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NextData {
|
||||||
|
props: {
|
||||||
|
pageProps: {
|
||||||
|
apolloState: {
|
||||||
|
ROOT_QUERY: Query;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
params: Record<string, string>;
|
||||||
|
query: Record<string, string>;
|
||||||
|
jwtToken: string;
|
||||||
|
dataSource: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
legacyId: number;
|
||||||
|
title: string;
|
||||||
|
authors: string[];
|
||||||
|
avgRating: number;
|
||||||
|
ratingCount: number;
|
||||||
|
publicationYear: number;
|
||||||
|
editionCount: number;
|
||||||
|
coverImageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Goodreads {
|
||||||
|
async getNextData(legacyId: number): Promise<NextData> {
|
||||||
|
const url = "https://www.goodreads.com/book/show/" + legacyId;
|
||||||
|
const res = await requestUrl({ url });
|
||||||
|
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
||||||
|
let nextDataRaw = doc.getElementById("__NEXT_DATA__")?.textContent;
|
||||||
|
if (typeof nextDataRaw !== "string") {
|
||||||
|
throw new Error("Unable to find next data script in the document.");
|
||||||
|
}
|
||||||
|
const nextData = JSON.parse(nextDataRaw) as NextData;
|
||||||
|
|
||||||
|
return nextData;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractBookFromNextData(nextData: NextData, ref: Ref): OutputBook {
|
||||||
|
const apolloState = nextData.props.pageProps.apolloState;
|
||||||
|
const bookData = apolloState[ref.__ref] as Book;
|
||||||
|
|
||||||
|
const contributorEdges = [
|
||||||
|
bookData.primaryContributorEdge,
|
||||||
|
...bookData.secondaryContributorEdges,
|
||||||
|
];
|
||||||
|
const authors = contributorEdges
|
||||||
|
.filter((edge) => edge.role === "Author")
|
||||||
|
.map<Contributor>((edge) => apolloState[edge.node.__ref])
|
||||||
|
.map<Author>((contributor) => ({
|
||||||
|
id: contributor.id,
|
||||||
|
legacyId: contributor.legacyId,
|
||||||
|
name: contributor.name,
|
||||||
|
description: contributor.description,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let series: OutputSeries | null = null;
|
||||||
|
if (bookData.bookSeries.length > 0) {
|
||||||
|
const bookSeries = bookData.bookSeries[0];
|
||||||
|
const seriesData = apolloState[bookSeries.series.__ref] as Series;
|
||||||
|
series = {
|
||||||
|
title: seriesData.title,
|
||||||
|
position: parseInt(bookSeries.userPosition, 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: bookData.title,
|
||||||
|
description: bookData.description,
|
||||||
|
authors,
|
||||||
|
series,
|
||||||
|
publisher: bookData.details.publisher,
|
||||||
|
publishedAt: new Date(bookData.details.publicationTime),
|
||||||
|
genres: bookData.bookGenres.map((genre) => genre.genre.name),
|
||||||
|
coverImageUrl: bookData.imageUrl,
|
||||||
|
pageCount: bookData.details.numPages,
|
||||||
|
isbn: bookData.details.isbn,
|
||||||
|
isbn13: bookData.details.isbn13,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBookByLegacyId(legacyId: number): Promise<OutputBook> {
|
||||||
|
const nextData = await this.getNextData(legacyId);
|
||||||
|
const bookRef = nextData.props.pageProps.apolloState.ROOT_QUERY[
|
||||||
|
`getBookByLegacyId({"legacyId":"${legacyId}"})`
|
||||||
|
] as Ref | undefined;
|
||||||
|
if (bookRef === undefined) {
|
||||||
|
throw new Error("Could not find reference for book.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.extractBookFromNextData(nextData, bookRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(q: string): Promise<SearchResult[]> {
|
||||||
|
const url =
|
||||||
|
"https://www.goodreads.com/search?q=" + encodeURIComponent(q);
|
||||||
|
const res = await requestUrl({ url });
|
||||||
|
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
||||||
|
|
||||||
|
return Array.from(
|
||||||
|
doc.querySelectorAll(
|
||||||
|
"table.tableList tr[itemtype='http://schema.org/Book']"
|
||||||
|
)
|
||||||
|
).map<SearchResult>((el) => {
|
||||||
|
const legacyId = parseInt(
|
||||||
|
el.querySelector("div.u-anchorTarget")?.id ?? "",
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
const title =
|
||||||
|
el.querySelector("a.bookTitle")?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
const authors = Array.from(el.querySelectorAll("a.authorName")).map(
|
||||||
|
(a) => a.textContent?.trim() || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const avgRating = parseFloat(
|
||||||
|
el
|
||||||
|
.querySelector("span.minirating")
|
||||||
|
?.textContent?.match(/(\d+\.\d+) avg rating/)?.[1] ?? "0"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ratingCount = parseInt(
|
||||||
|
el
|
||||||
|
.querySelector("span.minirating")
|
||||||
|
?.textContent?.match(/(\d[\d,]*) ratings/)?.[1] ?? "0",
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
const publicationYear = parseInt(
|
||||||
|
el
|
||||||
|
.querySelector("span.greyText")
|
||||||
|
?.textContent?.match(/published (\d{4})/)?.[1] ?? "0",
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
const editionCount = parseInt(
|
||||||
|
el
|
||||||
|
.querySelector("span.greyText")
|
||||||
|
?.textContent?.match(/(\d+) editions/)?.[1] ?? "0",
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
const coverImageUrl =
|
||||||
|
el.querySelector("img.bookCover")?.getAttribute("src") || "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
legacyId,
|
||||||
|
title,
|
||||||
|
authors,
|
||||||
|
avgRating,
|
||||||
|
ratingCount,
|
||||||
|
publicationYear,
|
||||||
|
editionCount,
|
||||||
|
coverImageUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,272 +0,0 @@
|
||||||
import { requestUrl } from "obsidian";
|
|
||||||
import type {
|
|
||||||
Author,
|
|
||||||
Book as OutputBook,
|
|
||||||
Series as OutputSeries,
|
|
||||||
} from "../types";
|
|
||||||
|
|
||||||
interface Ref {
|
|
||||||
__ref: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BookContributorEdge {
|
|
||||||
__typename: "BookContributorEdge";
|
|
||||||
node: Ref;
|
|
||||||
role: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BookSeries {
|
|
||||||
__typename: "BookSeries";
|
|
||||||
userPosition: string;
|
|
||||||
series: Ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Genre {
|
|
||||||
__typename: "Genre";
|
|
||||||
name: string;
|
|
||||||
webUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BookGenre {
|
|
||||||
__typename: "BookGenre";
|
|
||||||
genre: Genre;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Language {
|
|
||||||
__typename: "Language";
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BookDetails {
|
|
||||||
__typename: "BookDetails";
|
|
||||||
asin: string;
|
|
||||||
format: string;
|
|
||||||
numPages: number;
|
|
||||||
publicationTime: number;
|
|
||||||
publisher: string;
|
|
||||||
isbn: string;
|
|
||||||
isbn13: string;
|
|
||||||
language: Language;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Book {
|
|
||||||
__typename: "Book";
|
|
||||||
id: string;
|
|
||||||
legacyId: number;
|
|
||||||
webUrl: string;
|
|
||||||
title: string;
|
|
||||||
titleComplete: string;
|
|
||||||
description: string;
|
|
||||||
primaryContributorEdge: BookContributorEdge;
|
|
||||||
secondaryContributorEdges: BookContributorEdge[];
|
|
||||||
imageUrl: string;
|
|
||||||
bookSeries: BookSeries[];
|
|
||||||
bookGenres: BookGenre[];
|
|
||||||
details: BookDetails;
|
|
||||||
work: Ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContributorWorksConnection {
|
|
||||||
__typename: "ContributorWorksConnection";
|
|
||||||
totalCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContributorFollowersConnection {
|
|
||||||
__typename: "ContributorFollowersConnection";
|
|
||||||
totalCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Contributor {
|
|
||||||
__typename: "Contributor";
|
|
||||||
id: string;
|
|
||||||
legacyId: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
isGrAuthor: boolean;
|
|
||||||
works: ContributorWorksConnection;
|
|
||||||
profileImageUrl: string;
|
|
||||||
webUrl: string;
|
|
||||||
followers: ContributorFollowersConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Series {
|
|
||||||
__typename: "Series";
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
webUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Work {
|
|
||||||
__typename: "Work";
|
|
||||||
id: string;
|
|
||||||
legacyId: number;
|
|
||||||
bestBook: Ref;
|
|
||||||
// ...Book
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Query {
|
|
||||||
__typename: "Query";
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NextData {
|
|
||||||
props: {
|
|
||||||
pageProps: {
|
|
||||||
apolloState: {
|
|
||||||
ROOT_QUERY: Query;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
params: Record<string, string>;
|
|
||||||
query: Record<string, string>;
|
|
||||||
jwtToken: string;
|
|
||||||
dataSource: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchResult {
|
|
||||||
legacyId: number;
|
|
||||||
title: string;
|
|
||||||
authors: string[];
|
|
||||||
avgRating: number;
|
|
||||||
ratingCount: number;
|
|
||||||
publicationYear: number;
|
|
||||||
editionCount: number;
|
|
||||||
coverImageUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBookFromNextData(
|
|
||||||
nextData: NextData,
|
|
||||||
ref: Ref
|
|
||||||
): OutputBook {
|
|
||||||
const apolloState = nextData.props.pageProps.apolloState;
|
|
||||||
const bookData = apolloState[ref.__ref] as Book;
|
|
||||||
|
|
||||||
const contributorEdges = [
|
|
||||||
bookData.primaryContributorEdge,
|
|
||||||
...bookData.secondaryContributorEdges,
|
|
||||||
];
|
|
||||||
const authors = contributorEdges
|
|
||||||
.filter((edge) => edge.role === "Author")
|
|
||||||
.map<Contributor>((edge) => apolloState[edge.node.__ref])
|
|
||||||
.map<Author>((contributor) => ({
|
|
||||||
id: contributor.id,
|
|
||||||
legacyId: contributor.legacyId,
|
|
||||||
name: contributor.name,
|
|
||||||
description: contributor.description,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let series: OutputSeries | null = null;
|
|
||||||
if (bookData.bookSeries.length > 0) {
|
|
||||||
const bookSeries = bookData.bookSeries[0];
|
|
||||||
const seriesData = apolloState[bookSeries.series.__ref] as Series;
|
|
||||||
series = {
|
|
||||||
title: seriesData.title,
|
|
||||||
position: parseInt(bookSeries.userPosition, 10),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: bookData.title,
|
|
||||||
description: bookData.description,
|
|
||||||
authors,
|
|
||||||
series,
|
|
||||||
publisher: bookData.details.publisher,
|
|
||||||
publishedAt: new Date(bookData.details.publicationTime),
|
|
||||||
genres: bookData.bookGenres.map((genre) => genre.genre.name),
|
|
||||||
coverImageUrl: bookData.imageUrl,
|
|
||||||
pageCount: bookData.details.numPages,
|
|
||||||
isbn: bookData.details.isbn,
|
|
||||||
isbn13: bookData.details.isbn13,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNextData(legacyId: number): Promise<NextData> {
|
|
||||||
const url = "https://www.goodreads.com/book/show/" + legacyId;
|
|
||||||
const res = await requestUrl({ url });
|
|
||||||
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
|
||||||
let nextDataRaw = doc.getElementById("__NEXT_DATA__")?.textContent;
|
|
||||||
if (typeof nextDataRaw !== "string") {
|
|
||||||
throw new Error("Unable to find next data script in the document.");
|
|
||||||
}
|
|
||||||
const nextData = JSON.parse(nextDataRaw) as NextData;
|
|
||||||
|
|
||||||
return nextData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getBookByLegacyId(legacyId: number): Promise<OutputBook> {
|
|
||||||
const nextData = await getNextData(legacyId);
|
|
||||||
const bookRef = nextData.props.pageProps.apolloState.ROOT_QUERY[
|
|
||||||
`getBookByLegacyId({"legacyId":"${legacyId}"})`
|
|
||||||
] as Ref | undefined;
|
|
||||||
if (bookRef === undefined) {
|
|
||||||
throw new Error("Could not find reference for book.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return createBookFromNextData(nextData, bookRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function searchBooks(q: string): Promise<SearchResult[]> {
|
|
||||||
const url = "https://www.goodreads.com/search?q=" + encodeURIComponent(q);
|
|
||||||
const res = await requestUrl({ url });
|
|
||||||
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
|
||||||
|
|
||||||
return Array.from(
|
|
||||||
doc.querySelectorAll(
|
|
||||||
"table.tableList tr[itemtype='http://schema.org/Book']"
|
|
||||||
)
|
|
||||||
).map<SearchResult>((el) => {
|
|
||||||
const legacyId = parseInt(
|
|
||||||
el.querySelector("div.u-anchorTarget")?.id ?? "",
|
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
const title =
|
|
||||||
el.querySelector("a.bookTitle")?.textContent?.trim() || "";
|
|
||||||
|
|
||||||
const authors = Array.from(el.querySelectorAll("a.authorName")).map(
|
|
||||||
(a) => a.textContent?.trim() || ""
|
|
||||||
);
|
|
||||||
|
|
||||||
const avgRating = parseFloat(
|
|
||||||
el
|
|
||||||
.querySelector("span.minirating")
|
|
||||||
?.textContent?.match(/(\d+\.\d+) avg rating/)?.[1] ?? "0"
|
|
||||||
);
|
|
||||||
|
|
||||||
const ratingCount = parseInt(
|
|
||||||
el
|
|
||||||
.querySelector("span.minirating")
|
|
||||||
?.textContent?.match(/(\d[\d,]*) ratings/)?.[1] ?? "0",
|
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
const publicationYear = parseInt(
|
|
||||||
el
|
|
||||||
.querySelector("span.greyText")
|
|
||||||
?.textContent?.match(/published (\d{4})/)?.[1] ?? "0",
|
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
const editionCount = parseInt(
|
|
||||||
el
|
|
||||||
.querySelector("span.greyText")
|
|
||||||
?.textContent?.match(/(\d+) editions/)?.[1] ?? "0",
|
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
const coverImageUrl =
|
|
||||||
el.querySelector("img.bookCover")?.getAttribute("src") || "";
|
|
||||||
|
|
||||||
return {
|
|
||||||
legacyId,
|
|
||||||
title,
|
|
||||||
authors,
|
|
||||||
avgRating,
|
|
||||||
ratingCount,
|
|
||||||
publicationYear,
|
|
||||||
editionCount,
|
|
||||||
coverImageUrl,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
16
src/main.ts
16
src/main.ts
|
@ -17,12 +17,14 @@ import { LogReadingFinishedCommand } from "@commands/LogReadingFinishedCommand";
|
||||||
import { ResetReadingStatusCommand } from "@commands/ResetReadingStatusCommand";
|
import { ResetReadingStatusCommand } from "@commands/ResetReadingStatusCommand";
|
||||||
import { BackupReadingLogCommand } from "@commands/CreateReadingLogBackupCommand";
|
import { BackupReadingLogCommand } from "@commands/CreateReadingLogBackupCommand";
|
||||||
import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand";
|
import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand";
|
||||||
|
import { Goodreads } from "@data-sources/Goodreads";
|
||||||
|
|
||||||
export default class BookTrackerPlugin extends Plugin {
|
export default class BookTrackerPlugin extends Plugin {
|
||||||
settings: BookTrackerPluginSettings;
|
public settings: BookTrackerPluginSettings;
|
||||||
templater: Templater;
|
public templater: Templater;
|
||||||
storage: Storage;
|
public storage: Storage;
|
||||||
readingLog: ReadingLog;
|
public readingLog: ReadingLog;
|
||||||
|
public goodreads: Goodreads = new Goodreads();
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
@ -32,7 +34,11 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
this.readingLog = new ReadingLog(this.storage);
|
this.readingLog = new ReadingLog(this.storage);
|
||||||
|
|
||||||
this.addCommand(
|
this.addCommand(
|
||||||
new SearchGoodreadsCommand(this.app, this.createEntry.bind(this))
|
new SearchGoodreadsCommand(
|
||||||
|
this.app,
|
||||||
|
this.goodreads,
|
||||||
|
this.createEntry.bind(this)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
this.addCommand(new LogReadingStartedCommand(this.app, this.settings));
|
this.addCommand(new LogReadingStartedCommand(this.app, this.settings));
|
||||||
this.addCommand(
|
this.addCommand(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import GoodreadsSearchModalView from "./GoodreadsSearchModalView.svelte";
|
import GoodreadsSearchModalView from "./GoodreadsSearchModalView.svelte";
|
||||||
import { type SearchResult } from "@data-sources/goodreads";
|
import { Goodreads, type SearchResult } from "@data-sources/Goodreads";
|
||||||
import { App } from "obsidian";
|
import { App } from "obsidian";
|
||||||
import { SvelteModal } from "./SvelteModal";
|
import { SvelteModal } from "./SvelteModal";
|
||||||
|
|
||||||
|
@ -8,14 +8,23 @@ export class GoodreadsSearchModal extends SvelteModal<
|
||||||
> {
|
> {
|
||||||
constructor(
|
constructor(
|
||||||
app: App,
|
app: App,
|
||||||
|
goodreads: Goodreads,
|
||||||
onSearch: (error: any, results: SearchResult[]) => void = () => {}
|
onSearch: (error: any, results: SearchResult[]) => void = () => {}
|
||||||
) {
|
) {
|
||||||
super(app, GoodreadsSearchModalView, { props: { onSearch } });
|
super(app, GoodreadsSearchModalView, {
|
||||||
|
props: { goodreads, onSearch },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static createAndOpen(app: App): Promise<SearchResult[]> {
|
static createAndOpen(
|
||||||
|
app: App,
|
||||||
|
goodreads: Goodreads
|
||||||
|
): Promise<SearchResult[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const modal = new GoodreadsSearchModal(app, (error, results) => {
|
const modal = new GoodreadsSearchModal(
|
||||||
|
app,
|
||||||
|
goodreads,
|
||||||
|
(error, results) => {
|
||||||
modal.close();
|
modal.close();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -24,7 +33,8 @@ export class GoodreadsSearchModal extends SvelteModal<
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(results);
|
resolve(results);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
modal.open();
|
modal.open();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { searchBooks, type SearchResult } from "@data-sources/goodreads";
|
import { Goodreads, type SearchResult } from "@data-sources/Goodreads";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
goodreads: Goodreads;
|
||||||
onSearch: (error: any, results?: SearchResult[]) => void;
|
onSearch: (error: any, results?: SearchResult[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onSearch }: Props = $props();
|
let { goodreads, onSearch }: Props = $props();
|
||||||
|
|
||||||
let query = $state("");
|
let query = $state("");
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
const results = await searchBooks(query);
|
const results = await goodreads.search(query);
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
onSearch(new Error("No results found."));
|
onSearch(new Error("No results found."));
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import GoodreadsSearchSuggestion from "./GoodreadsSearchSuggestion.svelte";
|
import GoodreadsSearchSuggestion from "./GoodreadsSearchSuggestion.svelte";
|
||||||
import { type SearchResult } from "@data-sources/goodreads";
|
import { type SearchResult } from "@data-sources/Goodreads";
|
||||||
import { App, Notice, SuggestModal } from "obsidian";
|
import { App, SuggestModal } from "obsidian";
|
||||||
import { mount } from "svelte";
|
import { mount } from "svelte";
|
||||||
|
|
||||||
export class GoodreadsSearchSuggestModal extends SuggestModal<SearchResult> {
|
export class GoodreadsSearchSuggestModal extends SuggestModal<SearchResult> {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SearchResult } from "@data-sources/goodreads";
|
import type { SearchResult } from "@data-sources/Goodreads";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchResult: SearchResult;
|
searchResult: SearchResult;
|
||||||
|
@ -9,13 +9,19 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="obt-goodreads-search-suggestion">
|
<div class="obt-goodreads-search-suggestion">
|
||||||
<img class="cover" src={searchResult.coverImageUrl} alt={searchResult.title} />
|
<img
|
||||||
|
class="cover"
|
||||||
|
src={searchResult.coverImageUrl}
|
||||||
|
alt={searchResult.title}
|
||||||
|
/>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<h3 class="title">{searchResult.title}</h3>
|
<h3 class="title">{searchResult.title}</h3>
|
||||||
<p class="extra-details">
|
<p class="extra-details">
|
||||||
<span class="authors">{searchResult.authors.join(", ")}</span>
|
<span class="authors">{searchResult.authors.join(", ")}</span>
|
||||||
{#if searchResult.publicationYear > 0}
|
{#if searchResult.publicationYear > 0}
|
||||||
<span class="publicationYear">({searchResult.publicationYear})</span>
|
<span class="publicationYear"
|
||||||
|
>({searchResult.publicationYear})</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue