Skip to content

Commit e1885b6

Browse files
committed
feat: search books
1 parent 306e961 commit e1885b6

File tree

14 files changed

+239
-235
lines changed

14 files changed

+239
-235
lines changed

.prettierrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"semi": false,
3+
"singleQuote": true,
4+
"printWidth": 80
5+
}

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"editor.formatOnSave": true,
3+
"editor.codeActionsOnSave": ["source.fixAll.format", "source.fixAll.eslint"],
4+
"cSpell.words": ["Goodreads"]
5+
}

example/sample.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
const GoodReadsParser = require("../build/")
2-
1+
const GoodReadsParser = require('../build/')
32

43
;(async () => {
54
try {
6-
const data = await GoodReadsParser.search("dark matter")
7-
console.log("Book Data::", data);
8-
5+
const result = await GoodReadsParser.searchBooks({ q: 'Dark', page: 2 })
6+
console.log('result:', result)
97
} catch (error) {
10-
console.log("error", error);
8+
console.log('error', error)
119
}
12-
})();
10+
})()

global.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
interface String {
2+
toInt(): number
3+
toFloat(): number
4+
between(start: string, end: string): string
5+
escape(): string
6+
}

src/api/search-books.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { element } from '../parsers/element'
2+
import cover from '../utils/cover'
3+
import fetch from '../utils/fetch'
4+
5+
type SearchBooksProps = {
6+
q: string
7+
page?: number
8+
field?: 'title' | 'author' | 'genre'
9+
}
10+
11+
type SearchBooksResult = {
12+
page: number
13+
totalRecords: number
14+
books: SearchBookItem[]
15+
}
16+
17+
type SearchBookItem = {
18+
id: string
19+
url: string
20+
title: string
21+
author: string
22+
cover: string
23+
rating: number
24+
ratingCount: number
25+
publicationYear: number
26+
}
27+
28+
export default async function searchBooks({
29+
q,
30+
page,
31+
field,
32+
}: SearchBooksProps): Promise<SearchBooksResult> {
33+
const document = await fetch(`/search`, { q, page, field })
34+
35+
const resultInfo = element(document).query('.searchSubNavContainer').text()
36+
37+
const [pageInfo, totalInfo] = resultInfo.split('of about')
38+
39+
const trs = document.querySelectorAll('[itemtype^="http://schema.org/Book"]')
40+
41+
const books = Array.from(trs).map((tr) => {
42+
const el = element(tr)
43+
const ratingStr = el
44+
.query('td:nth-child(2) .minirating')
45+
.text()
46+
.split('avg rating — ')
47+
48+
return {
49+
title: el.query('td:first-of-type a')?.attr('title'),
50+
author: el.query('.authorName__container').text(),
51+
rating: ratingStr[0].toFloat(),
52+
ratingCount: ratingStr[1].toInt(),
53+
cover: cover(el.query('td:first-of-type img').attr('src')),
54+
publicationYear: el
55+
.query('.greyText.smallText.uitext')
56+
.textContent()
57+
.between('published', '—')
58+
.toInt(),
59+
url: 'https://goodreads.com/' + el.query('.bookTitle').attr('href'),
60+
id: el
61+
.query('.bookTitle')
62+
.attr('href')
63+
.split('&')[0]
64+
.replace('/book/show/', ''),
65+
}
66+
})
67+
68+
return {
69+
page: pageInfo.toInt(),
70+
totalRecords: totalInfo.split('results')[0].toInt(),
71+
books,
72+
}
73+
}

src/index.ts

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,4 @@
1-
import axios from "axios"
2-
import jsdom from "jsdom"
3-
import { getIdAndUrl, parseBookPage, parseSearchResult } from "./utils"
1+
import './utils/number'
2+
import searchBooks from './api/search-books'
43

5-
const { JSDOM } = jsdom;
6-
7-
async function parseBook(url: string) {
8-
const response = await axios.get(url);
9-
const result = parseBookPage(new JSDOM(response.data).window.document);
10-
return { ...result, ...getIdAndUrl(response) }
11-
}
12-
13-
export async function parseByISBN13(isbn13: string){
14-
try {
15-
return await parseBook("https://www.goodreads.com/search?q="+encodeURIComponent(isbn13))
16-
} catch (error) {
17-
throw error;
18-
}
19-
};
20-
21-
export async function parseByURL(url: string){
22-
try {
23-
return await parseBook(url)
24-
} catch (error) {
25-
throw error;
26-
}
27-
};
28-
29-
export async function search(term: string){
30-
try {
31-
const response = await axios.get("https://www.goodreads.com/search?q="+encodeURIComponent(term));
32-
const result = parseSearchResult(new JSDOM(response.data).window.document);
33-
return { result }
34-
} catch (error) {
35-
throw error;
36-
}
37-
};
4+
export { searchBooks }

src/parsers/element.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { escape } from '../utils/string'
2+
3+
export function element(el: Document | Element | null) {
4+
return {
5+
query: (selector: string) => element(el.querySelector(selector)),
6+
attr: (attribute: string) => {
7+
if (el.getAttribute) {
8+
return escape(el.getAttribute(attribute))
9+
}
10+
},
11+
innerHTML: () => el.innerHTML,
12+
innerText: () => el.innerText,
13+
textContent: () => el.textContent,
14+
text: () => escape(el.textContent),
15+
textUnsafe: () => el.textContent,
16+
}
17+
}

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface Book {
2+
id: string
3+
url: string
4+
title: string
5+
author: string
6+
description: string
7+
cover: string
8+
isbn: number | null
9+
isbn13: number | null
10+
}

src/utils.ts

Lines changed: 0 additions & 190 deletions
This file was deleted.

src/utils/cover.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function cover(url: string) {
2+
return url.replace('._SY75_', '')
3+
}

0 commit comments

Comments
 (0)