forked from mdluo/goodreads-box
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor the code to follow a more modular design
- Put all goodreads api related methods in a dedicated class - Put all github api related methods in a dedicated class - Put all output formatting in a dedicated class - Prepare the ground for dependency injection and more increased testability - Didn't change any behavior This commit is intended to prepare future work. Design choices made here are also a matter of preference.
- Loading branch information
1 parent
4d7ad3d
commit 8e24697
Showing
6 changed files
with
161 additions
and
136 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
|
||
|
||
export default class Formatter { | ||
|
||
public generateLines( | ||
books: Book[], | ||
MAX_LENGTH = 54, | ||
MAX_LINES = 5): String[] { | ||
const barWidth = Math.floor(MAX_LENGTH / 4) | ||
const lines = books.slice(0, MAX_LINES).map(({ title, percent }) => { | ||
const bar = this.generateBarChart(percent, barWidth) | ||
const percentage = `${percent}%`.padStart(4, ' ') | ||
const length = MAX_LENGTH - bar.length - percentage.length - 1 | ||
let text = title | ||
if (title.length > length) { | ||
text = title.substring(0, length - 3).concat('...') | ||
} else { | ||
text = title.padEnd(length, ' ') | ||
} | ||
return `${text} ${bar}${percentage}` | ||
}) | ||
return lines | ||
} | ||
|
||
/** | ||
* Copyright (c) 2019, Matan Kushner <[email protected]> | ||
* https://github.com/matchai/waka-box/blob/master/index.js | ||
*/ | ||
private generateBarChart(percent: number, size: number): string { | ||
const syms = '░▏▎▍▌▋▊▉█'; | ||
|
||
const frac = Math.floor((size * 8 * percent) / 100); | ||
const barsFull = Math.floor(frac / 8); | ||
if (barsFull >= size) { | ||
return syms.substring(8, 9).repeat(size); | ||
} | ||
const semi = frac % 8; | ||
|
||
return [syms.substring(8, 9).repeat(barsFull), syms.substring(semi, semi + 1)] | ||
.join('') | ||
.padEnd(size, syms.substring(0, 1)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Octokit } from '@octokit/rest' | ||
|
||
|
||
export default class Github { | ||
|
||
private octokit = new Octokit({ auth: process.env.GH_TOKEN }); | ||
|
||
async updateGist( | ||
title: string, | ||
content: string | ||
): Promise<string> { | ||
const gist_id = process.env.GIST_ID || ''; | ||
const gist = await this.octokit.gists.get({ gist_id }); | ||
const filename = Object.keys(gist.data.files)[0]; | ||
await this.octokit.gists.update({ | ||
gist_id, | ||
files: { | ||
[filename]: { | ||
filename: title, | ||
content, | ||
}, | ||
}, | ||
}); | ||
return gist.url; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import xml from 'fast-xml-parser' | ||
import he from 'he' | ||
import got from 'got' | ||
|
||
export default class Goodreads { | ||
|
||
private request = got.extend({ | ||
prefixUrl: 'https://www.goodreads.com', | ||
searchParams: { | ||
v: 2, | ||
key: process.env.GOODREADS_API_KEY, | ||
}, | ||
}) | ||
|
||
public async getBooks(): Promise<Book[]> { | ||
let reviewList: Goodreads.Review[] | ||
try { | ||
reviewList = await this.getReviewList() | ||
} catch (error) { | ||
throw new Error("Error while fetching review list: " + error) | ||
} | ||
|
||
const reviews = await Promise.all( | ||
reviewList.map(({ id }) => this.getReviewShow(id)) | ||
) | ||
|
||
const books = reviews | ||
.map(({ id, book, date_updated, user_statuses }) => { | ||
const latestStatus = Array.isArray(user_statuses?.user_status) | ||
? user_statuses?.user_status[0] | ||
: user_statuses?.user_status | ||
return { | ||
id: id, | ||
title: he.decode(book.title), | ||
percent: latestStatus?.percent || 0, | ||
date: new Date(latestStatus?.updated_at || date_updated), | ||
} | ||
}) | ||
.sort((a, b) => { | ||
if (process.env.BOOKS_SORT_BY === 'percent') { | ||
return b.percent - a.percent | ||
} | ||
return b.date.getTime() - a.date.getTime() | ||
}) | ||
return books | ||
} | ||
|
||
private async getReviewList( | ||
shelf = 'currently-reading' | ||
): Promise<Goodreads.Review[]> { | ||
const res = await this.request | ||
.get('review/list', { | ||
searchParams: { | ||
id: process.env.GOODREADS_USER_ID, | ||
shelf, | ||
}, | ||
}) | ||
.text() | ||
const reviewList: Goodreads.ReviewList = xml.parse(res) | ||
const reviews = reviewList.GoodreadsResponse.reviews.review | ||
return reviews | ||
} | ||
|
||
private async getReviewShow(id: number): Promise<Goodreads.Review> { | ||
const res = await this.request.get('review/show', { searchParams: { id } }).text() | ||
const reviewShow: Goodreads.ReviewShow = xml.parse(res) | ||
const review = reviewShow.GoodreadsResponse.review | ||
return review | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,31 @@ | ||
import dotenv from 'dotenv'; | ||
import he from 'he'; | ||
import dotenv from 'dotenv' | ||
import Formatter from './formatter' | ||
import Github from './github' | ||
import Goodreads from './goodreads' | ||
|
||
import * as api from './api'; | ||
import { generateBarChart } from './bar'; | ||
dotenv.config() | ||
|
||
dotenv.config(); | ||
|
||
const MAX_LENGTH = 54; | ||
const MAX_LINES = 5; | ||
async function main() { | ||
try { | ||
const goodreads = new Goodreads() | ||
|
||
async function getBooks(): Promise<Book[]> { | ||
const reviewList = await api.getReviewList(); | ||
const reviews = await Promise.all( | ||
reviewList.map(({ id }) => api.getReviewShow(id)) | ||
); | ||
const books = reviews | ||
.map(({ id, book, date_updated, user_statuses }) => { | ||
const latestStatus = Array.isArray(user_statuses?.user_status) | ||
? user_statuses?.user_status[0] | ||
: user_statuses?.user_status; | ||
return { | ||
id: id, | ||
title: he.decode(book.title), | ||
percent: latestStatus?.percent || 0, | ||
date: new Date(latestStatus?.updated_at || date_updated), | ||
}; | ||
}) | ||
.sort((a, b) => { | ||
if (process.env.BOOKS_SORT_BY === 'percent') { | ||
return b.percent - a.percent; | ||
} | ||
return b.date.getTime() - a.date.getTime(); | ||
}); | ||
return books; | ||
} | ||
const books = await goodreads.getBooks() | ||
console.log(`Found ${books.length} book(s)`) | ||
|
||
function generateLines(books: Book[]) { | ||
const barWidth = Math.floor(MAX_LENGTH / 4); | ||
const lines = books.slice(0, MAX_LINES).map(({ title, percent }) => { | ||
const bar = generateBarChart(percent, barWidth); | ||
const percentage = `${percent}%`.padStart(4, ' '); | ||
const length = MAX_LENGTH - bar.length - percentage.length - 1; | ||
let text = title; | ||
if (title.length > length) { | ||
text = title.substring(0, length - 3).concat('...'); | ||
} else { | ||
text = title.padEnd(length, ' '); | ||
} | ||
return `${text} ${bar}${percentage}`; | ||
}); | ||
return lines; | ||
} | ||
const formatter = new Formatter() | ||
const lines = formatter.generateLines(books) | ||
|
||
(async () => { | ||
try { | ||
const books = await getBooks(); | ||
console.log(`Found ${books.length} book(s)`); | ||
const lines = generateLines(books); | ||
const url = await api.updateGist( | ||
const github = new Github() | ||
const url = await github.updateGist( | ||
`📚 Currently reading books (${lines.length}/${books.length})`, | ||
lines.join('\n') | ||
); | ||
console.log(`Updated: ${url}`); | ||
) | ||
|
||
console.log(`Updated: ${url}`) | ||
} catch (error) { | ||
console.error(error); | ||
process.exit(1); | ||
console.error(error) | ||
process.exit(1) | ||
} | ||
})(); | ||
} | ||
|
||
main() |