diff --git a/src/book.ts b/src/book.ts index ef57825..883b7b6 100644 --- a/src/book.ts +++ b/src/book.ts @@ -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; @@ -31,6 +37,7 @@ export class KindleBook { this.#client = client; this.#version = version ?? "2000010"; + this.#baseUrl = baseUrl; } /** @@ -41,7 +48,7 @@ export class KindleBook { */ async details(): Promise { 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}` ); diff --git a/src/fetch-books.ts b/src/fetch-books.ts index 4a56957..c9c636d 100644 --- a/src/fetch-books.ts +++ b/src/fetch-books.ts @@ -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[]; @@ -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, diff --git a/src/kindle.ts b/src/kindle.ts index db3aa99..6791b2c 100644 --- a/src/kindle.ts +++ b/src/kindle.ts @@ -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 = { @@ -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, @@ -81,6 +96,7 @@ export class Kindle { */ readonly defaultBooks: KindleBook[]; readonly #client: HttpClient; + readonly #baseUrl: string; constructor( private options: KindleOptions, @@ -91,9 +107,11 @@ export class Kindle { ) { this.defaultBooks = prePopulatedBooks ?? []; this.#client = client; + this.#baseUrl = options.baseUrl; } static async fromConfig(config: KindleConfiguration): Promise { + const baseUrl = new URL(config.baseUrl ?? Kindle.BASE_URL).origin; const cookies = typeof config.cookies === "string" ? Kindle.deserializeCookies(config.cookies) @@ -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( @@ -115,6 +137,7 @@ export class Kindle { cookies, }, sessionId, + baseUrl, }, client, books @@ -122,6 +145,7 @@ export class Kindle { } static async deviceToken( + baseUrl: string, client: HttpClient, token: string ): Promise { @@ -129,12 +153,13 @@ export class Kindle { 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?: { @@ -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 ); @@ -187,7 +213,12 @@ export class Kindle { query?: Query; filter?: Filter; }): Promise { - 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