Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor and fix handling of goodreads api when currently reading contains a single item #2

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 0 additions & 58 deletions src/api.ts

This file was deleted.

18 changes: 0 additions & 18 deletions src/bar.ts

This file was deleted.

43 changes: 43 additions & 0 deletions src/formatter.ts
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));
}
}
26 changes: 26 additions & 0 deletions src/github.ts
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;
}
}
73 changes: 73 additions & 0 deletions src/goodreads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import xml from 'fast-xml-parser'
import he from 'he'
import got from 'got'
import { ensureArray } from './type-utils'

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)
// reviews.review can be an array or an item
const reviews = ensureArray(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
}
}
82 changes: 22 additions & 60 deletions src/index.ts
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()
7 changes: 7 additions & 0 deletions src/type-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

export function ensureArray<T>(v: T | T[]): T[] {
if (Array.isArray(v)) {
return v
}
return [v]
}
2 changes: 1 addition & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ declare namespace Goodreads {
interface ReviewList {
GoodreadsResponse: {
reviews: {
review: Review[];
review: Review[] | Review;
};
};
}
Expand Down