Skip to content
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
13 changes: 10 additions & 3 deletions src/book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ export class KindleBook {

readonly #client: HttpClient;
readonly #version: string;

constructor(options: KindleBookData, client: HttpClient, version?: string) {
readonly #baseUrl: string;

constructor(
options: KindleBookData,
client: HttpClient,
baseUrl: string,
version?: string
) {
this.title = options.title;
this.authors = KindleBook.normalizeAuthors(options.authors);
this.imageUrl = options.productUrl;
Expand All @@ -31,6 +37,7 @@ export class KindleBook {

this.#client = client;
this.#version = version ?? "2000010";
this.#baseUrl = baseUrl;
}

/**
Expand All @@ -41,7 +48,7 @@ export class KindleBook {
*/
async details(): Promise<KindleBookLightDetails> {
const response = await this.#client.request(
`https://read.amazon.com/service/mobile/reader/startReading?asin=${
`${this.#baseUrl}/service/mobile/reader/startReading?asin=${
this.asin
}&clientVersion=${this.#version}`
);
Expand Down
9 changes: 6 additions & 3 deletions src/fetch-books.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Query, Filter } from "./query-filter.js";
export async function fetchBooks(
client: HttpClient,
url: string,
baseUrl: string,
version?: string
): Promise<{
books: KindleBook[];
Expand All @@ -23,14 +24,16 @@ export async function fetchBooks(

const body = JSON.parse(resp.body) as Response;
return {
books: body.itemsList.map((book) => new KindleBook(book, client, version)),
books: body.itemsList.map(
(book) => new KindleBook(book, client, baseUrl, version)
),
sessionId,
paginationToken: body.paginationToken,
};
}

export function toUrl(query: Query, filter: Filter): string {
const url = new URL(Kindle.BOOKS_URL);
export function toUrl(baseUrl: string, query: Query, filter: Filter): string {
const url = new URL(`${baseUrl}/${Kindle.BOOKS_PATH}`);
const searchParams = {
...query,
...filter,
Expand Down
49 changes: 40 additions & 9 deletions src/kindle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,21 @@ export type KindleConfiguration = {
cookies: KindleRequiredCookies,
clientOptions: TlsClientConfig
) => HttpClient;

/**
* Base url of the kindle service.
* Amazon has different regional service endpoints, for example https://lesen.amazon.de/kindle-library (DACH region) or https://read.amazon.com/kindle-library (worldwide).
* Path and query parameters will be ignored.
*
* @default "https://read.amazon.com"
*/
baseUrl?: string;
};

export type KindleOptions = {
config: KindleConfiguration;
sessionId: string;
baseUrl: string;
};

export type KindleFromCookieOptions = {
Expand All @@ -60,13 +70,18 @@ export type KindleFromCookieOptions = {
};

export class Kindle {
public static DEVICE_TOKEN_URL =
"https://read.amazon.com/service/web/register/getDeviceToken";
public static readonly BOOKS_URL =
"https://read.amazon.com/kindle-library/search?query=&libraryType=BOOKS&sortType=recency&querySize=50";
public static readonly BASE_URL = "https://read.amazon.com";

public static readonly DEVICE_TOKEN_PATH =
"service/web/register/getDeviceToken";

public static readonly BOOKS_PATH =
"kindle-library/search?query=&libraryType=BOOKS&sortType=recency&querySize=50";

public static readonly DEFAULT_QUERY = Object.freeze({
sortType: "acquisition_desc",
} satisfies Query);

public static readonly DEFAULT_FILTER = Object.freeze({
querySize: 50,
fetchAllPages: false,
Expand All @@ -81,6 +96,7 @@ export class Kindle {
*/
readonly defaultBooks: KindleBook[];
readonly #client: HttpClient;
readonly #baseUrl: string;

constructor(
private options: KindleOptions,
Expand All @@ -91,9 +107,11 @@ export class Kindle {
) {
this.defaultBooks = prePopulatedBooks ?? [];
this.#client = client;
this.#baseUrl = options.baseUrl;
}

static async fromConfig(config: KindleConfiguration): Promise<Kindle> {
const baseUrl = new URL(config.baseUrl ?? Kindle.BASE_URL).origin;
const cookies =
typeof config.cookies === "string"
? Kindle.deserializeCookies(config.cookies)
Expand All @@ -102,10 +120,14 @@ export class Kindle {
config.clientFactory?.(cookies, config.tlsServer) ??
new HttpClient(cookies, config.tlsServer);

const { sessionId, books } = await Kindle.baseRequest(client);
const { sessionId, books } = await Kindle.baseRequest(baseUrl, client);
client.updateSession(sessionId);

const deviceInfo = await Kindle.deviceToken(client, config.deviceToken);
const deviceInfo = await Kindle.deviceToken(
baseUrl,
client,
config.deviceToken
);
client.updateAdpSession(deviceInfo.deviceSessionToken);

return new this(
Expand All @@ -115,26 +137,29 @@ export class Kindle {
cookies,
},
sessionId,
baseUrl,
},
client,
books
);
}

static async deviceToken(
baseUrl: string,
client: HttpClient,
token: string
): Promise<KindleDeviceInfo> {
const params = new URLSearchParams({
serialNumber: token,
deviceType: token,
});
const url = `${Kindle.DEVICE_TOKEN_URL}?${params.toString()}`;
const url = `${baseUrl}/${Kindle.DEVICE_TOKEN_PATH}?${params.toString()}`;
const response = await client.request(url);
return JSON.parse(response.body) as KindleDeviceInfo;
}

static async baseRequest(
baseUrl: string,
client: HttpClient,
version?: string,
args?: {
Expand All @@ -159,10 +184,11 @@ export class Kindle {

// loop until we get less than the requested amount of books or hit the limit
do {
const url = toUrl(query, filter);
const url = toUrl(baseUrl, query, filter);
const { books, sessionId, paginationToken } = await fetchBooks(
client,
url,
baseUrl,
version
);

Expand All @@ -187,7 +213,12 @@ export class Kindle {
query?: Query;
filter?: Filter;
}): Promise<KindleBook[]> {
const result = await Kindle.baseRequest(this.#client, undefined, args);
const result = await Kindle.baseRequest(
this.#baseUrl,
this.#client,
undefined,
args
);
// refreshing the internal session every time books is called.
// This doesn't prevent us from calling the books endpoint but
// it does prevent requesting the metadata of individual books
Expand Down