From 732fc74d199a4acadd1372af9533b1ef27fb51f1 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:49:23 +0200 Subject: [PATCH 1/6] Add subjects as genres to OpenLibraryAPI --- src/api/apis/OpenLibraryAPI.ts | 4 +++- src/models/BookModel.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/apis/OpenLibraryAPI.ts b/src/api/apis/OpenLibraryAPI.ts index f42f7918..391821e0 100644 --- a/src/api/apis/OpenLibraryAPI.ts +++ b/src/api/apis/OpenLibraryAPI.ts @@ -24,6 +24,7 @@ interface SearchResponse { first_publish_year?: number; key: string; description?: string; + subject?: string[]; number_of_pages_median?: number; isbn?: string[]; @@ -94,7 +95,7 @@ export class OpenLibraryAPI extends APIModel { params: { query: { q: `${id}`, - fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description', + fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description,subject*', }, }, fetch: obsidianFetch, @@ -142,6 +143,7 @@ export class OpenLibraryAPI extends APIModel { author: result.author_name?.join(', '), plot: result.description ?? undefined, + genres: result.subject ?? undefined, pages: Number.isNaN(pages) ? undefined : pages, onlineRating: result.ratings_average, image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined, diff --git a/src/models/BookModel.ts b/src/models/BookModel.ts index e75da93d..a223f490 100644 --- a/src/models/BookModel.ts +++ b/src/models/BookModel.ts @@ -8,6 +8,7 @@ export type BookData = ModelToData; export class BookModel extends MediaTypeModel { author: string; plot: string; + genres: string[]; pages: number; image: string; onlineRating: number; @@ -27,6 +28,7 @@ export class BookModel extends MediaTypeModel { this.author = ''; this.plot = ''; + this.genres= []; this.pages = 0; this.image = ''; this.onlineRating = 0; From f03e3bf4420b372c5a9defee60e3893e730b2cba Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:03:20 +0200 Subject: [PATCH 2/6] Add genres again --- packages/obsidian/src/api/apis/OpenLibraryAPI.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts index 08df3be2..b43a7049 100644 --- a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts +++ b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts @@ -29,6 +29,7 @@ interface SearchResponse { first_publish_year?: number; key: string; description?: string; + subject?: string[]; number_of_pages_median?: number; isbn?: string[]; @@ -119,7 +120,7 @@ export class OpenLibraryAPI extends APIModel { params: { query: { q: `${id}`, - fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description', + fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description,subject', }, }, fetch: obsidianFetch, @@ -194,6 +195,7 @@ export class OpenLibraryAPI extends APIModel { author: result.author_name?.join(', '), plot: result.description ?? undefined, + genres: result.subject ?? undefined, pages: Number.isNaN(pages) ? undefined : pages, onlineRating: result.ratings_average, image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined, From bc53fdfd22f49641ce00cfff03b6712388b12dbd Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:08:07 +0200 Subject: [PATCH 3/6] Rewrote OpenLibraryAPI * Split general book search and specific editions search (when using /books/ID in searchByID) * When searching for specific editions if there's missing metadata it'll use search.json to fill in the missing info * ran bun check fix --- .../obsidian/src/api/apis/OpenLibraryAPI.ts | 270 +++++++++++++----- packages/obsidian/src/models/BookModel.ts | 2 +- 2 files changed, 197 insertions(+), 75 deletions(-) diff --git a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts index b43a7049..b92cb2b9 100644 --- a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts +++ b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts @@ -12,36 +12,42 @@ import { err, fromPromise, ok } from 'packages/obsidian/src/utils/result'; import { obsidianFetch } from 'packages/obsidian/src/utils/Utils'; import type { paths } from 'packages/schemas/src/OpenLibrary'; +type OpenLibraryIdKind = 'book' | 'search'; + interface SearchResponse { - editions: { - docs: { - key?: string; - title?: string; - cover_i?: number; - isbn?: string[]; - }[]; - }; - cover_i?: number; - has_fulltext?: boolean; - edition_count?: number; + key?: string; title?: string; + cover_i?: number; author_name?: string[]; + author_key?: string[]; first_publish_year?: number; - key: string; - description?: string; + description?: string | { value?: string }; subject?: string[]; - number_of_pages_median?: number; - isbn?: string[]; + number_of_pages?: number; ratings_average?: number; + isbn?: string[]; +} + +interface BookResponse { + key?: string; + title?: string; + covers?: number[]; + isbn_10?: string[]; + isbn_13?: string[]; + authors?: { key?: string }[]; + works?: { key?: string }[]; + pagination?: string; + publish_date?: string; + number_of_pages?: number; } export class OpenLibraryAPI extends APIModel { plugin: MediaDbPlugin; + private client = createClient({ baseUrl: 'https://openlibrary.org/' }); constructor(plugin: MediaDbPlugin) { super(); - this.plugin = plugin; this.apiName = 'OpenLibraryAPI'; this.apiDescription = 'A free API for books'; @@ -49,16 +55,89 @@ export class OpenLibraryAPI extends APIModel { this.types = [MediaType.Book]; } + private detectIdKind(id: string): OpenLibraryIdKind { + if (/\/books\/OL\d+M/i.test(id)) return 'book'; + return 'search'; + } + + private normalizeId(id: string): string { + return id.startsWith('http') ? new URL(id).pathname : id; + } + + private pickDescription(desc?: string | { value?: string }): string | undefined { + if (!desc) return undefined; + return typeof desc === 'string' ? desc : desc.value; + } + + private async fetchOpenLibraryJson(url: string, context: Record): Promise> { + try { + const response = await obsidianFetch(new Request(`https://openlibrary.org${url}`)); + if (!response.ok) { + return err({ + kind: MDBErrorKind.Api, + message: `MDB | Received status code ${response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.status} from ${this.apiName}.`, + context: { ...context, apiName: this.apiName, status: response.status }, + }); + } + return ok((await response.json()) as T); + } catch (cause) { + return err( + toMdbError(cause, { + kind: MDBErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context, + }), + ); + } + } + + private async searchByOlid(olid: string): Promise> { + const responseResult = await fromPromise( + this.client.GET('/search.json', { + params: { + query: { + q: olid, + fields: 'key,title,author_name,author_key,first_publish_year,cover_i,subject,number_of_pages,number_of_pages_median,description,ratings_average,isbn', + }, + }, + fetch: obsidianFetch, + }), + cause => + toMdbError(cause, { + kind: MDBErrorKind.Network, + message: `MDB | Network error querying ${this.apiName}`, + userMessage: `Network error querying ${this.apiName}`, + context: { apiName: this.apiName, olid }, + }), + ); + + if (!responseResult.ok) return err(responseResult.error); + + const response = responseResult.value; + if (response.error !== undefined) { + return err({ + kind: MDBErrorKind.Api, + message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, + userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, + context: { apiName: this.apiName, status: response.response.status, olid }, + }); + } + + const data = response.data as { docs?: SearchResponse[] }; + return ok(data.docs?.[0]); + } + async searchByTitle(title: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by Title`); - const client = createClient({ baseUrl: 'https://openlibrary.org/' }); - const responseResult = await fromPromise( - client.GET('/search.json', { + this.client.GET('/search.json', { params: { query: { q: title, + fields: 'key,title,author_name,first_publish_year,cover_i,subject,number_of_pages,number_of_pages_median,description,ratings_average', }, }, fetch: obsidianFetch, @@ -86,23 +165,28 @@ export class OpenLibraryAPI extends APIModel { }); } - const data = response.data as { - docs: SearchResponse[]; - }; - - // console.debug(data); - + const data = response.data as { docs: SearchResponse[] }; const ret: MediaTypeModel[] = []; for (const result of data.docs) { + const isbn10 = result.isbn?.find(el => el.length <= 10); + const isbn13 = result.isbn?.find(el => el.length === 13); + ret.push( new BookModel({ title: result.title, englishTitle: result.title, year: result.first_publish_year?.toString() ?? 'unknown', dataSource: this.apiName, - id: result.key, + id: result.key ?? title, author: result.author_name?.join(', '), + plot: this.pickDescription(result.description), + genres: result.subject, + pages: result.number_of_pages_median ?? result.number_of_pages, + onlineRating: result.ratings_average, + isbn: isbn10 ? Number(isbn10) : undefined, + isbn13: isbn13 ? Number(isbn13) : undefined, + image: result.cover_i ? `https://covers.openlibrary.org/b/id/${result.cover_i}-L.jpg` : undefined, }), ); } @@ -113,14 +197,76 @@ export class OpenLibraryAPI extends APIModel { async getById(id: string): Promise> { Logger.log(`MDB | api "${this.apiName}" queried by ID`); - const client = createClient({ baseUrl: 'https://openlibrary.org/' }); + const normalizedId = this.normalizeId(id); + const kind = this.detectIdKind(normalizedId); + + if (kind === 'book') { + return this.getByBookId(normalizedId); + } + + return this.getBySearchQuery(normalizedId); + } + + private async getByBookId(bookKey: string): Promise> { + const bookResult = await this.fetchOpenLibraryJson(`${bookKey}.json`, { + apiName: this.apiName, + bookKey, + }); + if (!bookResult.ok) return err(bookResult.error); + + const book = bookResult.value; + const olid = bookKey.replace(/^\/books\//i, ''); + const searchResult = await this.searchByOlid(olid); + + const search = searchResult.ok ? searchResult.value : undefined; + + const title = book.title ?? search?.title ?? 'unknown'; + const coverId = book.covers?.[0] ?? search?.cover_i; + + const yearFromBook = book.publish_date; + const yearFromSearch = search?.first_publish_year?.toString(); + const year = yearFromBook ?? yearFromSearch ?? 'unknown'; + + const pagesFromBook = book.pagination ? Number(book.pagination) : book.number_of_pages; + const pages = Number.isFinite(pagesFromBook!) ? Number(pagesFromBook) : (search?.number_of_pages_median ?? search?.number_of_pages); + + const bookIsbn10 = book.isbn_10?.find(el => el.length <= 10); + const bookIsbn13 = book.isbn_13?.find(el => el.length === 13); + const searchIsbn10 = search?.isbn?.find(el => el.length <= 10); + const searchIsbn13 = search?.isbn?.find(el => el.length === 13); + + return ok( + new BookModel({ + title, + englishTitle: title, + year, + dataSource: this.apiName, + url: `https://openlibrary.org${bookKey}`, + id: bookKey, + isbn: bookIsbn10 ? Number(bookIsbn10) : searchIsbn10 ? Number(searchIsbn10) : undefined, + isbn13: bookIsbn13 ? Number(bookIsbn13) : searchIsbn13 ? Number(searchIsbn13) : undefined, + author: search?.author_name?.join(', '), + plot: this.pickDescription(search?.description), + genres: search?.subject, + pages: Number.isFinite(pages!) ? Number(pages) : undefined, + image: coverId ? `https://covers.openlibrary.org/b/id/${coverId}-L.jpg` : undefined, + released: true, + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, + }), + ); + } + private async getBySearchQuery(query: string): Promise> { const responseResult = await fromPromise( - client.GET('/search.json', { + this.client.GET('/search.json', { params: { query: { - q: `${id}`, - fields: 'key,title,author_name,number_of_pages_median,first_publish_year,isbn,ratings_score,first_sentence,title_suggest,rating*,cover*,editions,description,subject', + q: query, + fields: 'key,title,author_name,first_publish_year,cover_i,subject,number_of_pages,number_of_pages_median,description,ratings_average,isbn', }, }, fetch: obsidianFetch, @@ -130,78 +276,54 @@ export class OpenLibraryAPI extends APIModel { kind: MDBErrorKind.Network, message: `MDB | Network error querying ${this.apiName}`, userMessage: `Network error querying ${this.apiName}`, - context: { apiName: this.apiName, id }, + context: { apiName: this.apiName, query }, }), ); - if (!responseResult.ok) { - return err(responseResult.error); - } - const response = responseResult.value; + if (!responseResult.ok) return err(responseResult.error); + const response = responseResult.value; if (response.error !== undefined) { return err({ kind: MDBErrorKind.Api, message: `MDB | Received status code ${response.response.status} from ${this.apiName}.`, userMessage: `Received status code ${response.response.status} from ${this.apiName}.`, - context: { apiName: this.apiName, status: response.response.status, id }, + context: { apiName: this.apiName, status: response.response.status, query }, }); } - const data = response.data as { - docs: SearchResponse[]; - q?: string; - }; + const data = response.data as { docs: SearchResponse[] }; const result = data.docs?.[0]; if (!result) { return err({ kind: MDBErrorKind.Api, - message: `MDB | No data found for ID ${id} in ${this.apiName}.`, - userMessage: `No data found for ID ${id}.`, - context: { apiName: this.apiName, id }, + message: `MDB | No data found for query ${query} in ${this.apiName}.`, + userMessage: `No data found for query ${query}.`, + context: { apiName: this.apiName, query }, }); } - let key = result.key; - let title = result.title; - let cover_i = result.cover_i; - let isbnArr = result.isbn; - - // Check if the query is for /isbn/ or /books/ and extract from editions.docs if present - const q = data.q ?? ''; - if ((q.includes('/isbn/') || q.includes('/books/')) && result.editions && Array.isArray(result.editions.docs) && result.editions.docs.length > 0) { - const edition = result.editions.docs[0]; - key = edition.key ?? key; - title = edition.title ?? title; - cover_i = edition.cover_i ?? cover_i; - isbnArr = edition.isbn ?? isbnArr; - } - - const pages = Number(result.number_of_pages_median); - const isbn = Number((isbnArr ?? []).find((el: string) => el.length <= 10)); - const isbn13 = Number((isbnArr ?? []).find((el: string) => el.length == 13)); + const isbn10 = result.isbn?.find(el => el.length <= 10); + const isbn13 = result.isbn?.find(el => el.length === 13); return ok( new BookModel({ - title: title, + title: result.title, + englishTitle: result.title, year: result.first_publish_year?.toString() ?? 'unknown', dataSource: this.apiName, - url: `https://openlibrary.org` + key, - id: key, - isbn: Number.isNaN(isbn) ? undefined : isbn, - isbn13: Number.isNaN(isbn13) ? undefined : isbn13, - englishTitle: title, - + id: result.key ?? query, + url: result.key ? `https://openlibrary.org${result.key}` : undefined, + isbn: isbn10 ? Number(isbn10) : undefined, + isbn13: isbn13 ? Number(isbn13) : undefined, author: result.author_name?.join(', '), - plot: result.description ?? undefined, - genres: result.subject ?? undefined, - pages: Number.isNaN(pages) ? undefined : pages, + plot: this.pickDescription(result.description), + genres: result.subject, + pages: result.number_of_pages_median ?? result.number_of_pages, onlineRating: result.ratings_average, - image: cover_i ? `https://covers.openlibrary.org/b/id/` + cover_i + `-L.jpg` : undefined, - + image: result.cover_i ? `https://covers.openlibrary.org/b/id/${result.cover_i}-L.jpg` : undefined, released: true, - userData: { read: false, lastRead: '', diff --git a/packages/obsidian/src/models/BookModel.ts b/packages/obsidian/src/models/BookModel.ts index eac100e5..7d0aba72 100644 --- a/packages/obsidian/src/models/BookModel.ts +++ b/packages/obsidian/src/models/BookModel.ts @@ -28,7 +28,7 @@ export class BookModel extends MediaTypeModel { this.author = ''; this.plot = ''; - this.genres= []; + this.genres = []; this.pages = 0; this.image = ''; this.onlineRating = 0; From 338fea341fd79949708af4b31515ff02f6b35ece Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:18:00 +0200 Subject: [PATCH 4/6] reimplemented /isbn/ search + fixed amount of pages --- .../obsidian/src/api/apis/OpenLibraryAPI.ts | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts index b92cb2b9..e1f6d6c3 100644 --- a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts +++ b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts @@ -57,6 +57,7 @@ export class OpenLibraryAPI extends APIModel { private detectIdKind(id: string): OpenLibraryIdKind { if (/\/books\/OL\d+M/i.test(id)) return 'book'; + if (/\/isbn\/\d+/i.test(id)) return 'book'; return 'search'; } @@ -137,7 +138,7 @@ export class OpenLibraryAPI extends APIModel { params: { query: { q: title, - fields: 'key,title,author_name,first_publish_year,cover_i,subject,number_of_pages,number_of_pages_median,description,ratings_average', + fields: 'key,title,author_name,first_publish_year,cover_i,subject,number_of_pages,number_of_pages_median,description,ratings_average,isbn', }, }, fetch: obsidianFetch, @@ -151,11 +152,9 @@ export class OpenLibraryAPI extends APIModel { }), ); - if (!responseResult.ok) { - return err(responseResult.error); - } - const response = responseResult.value; + if (!responseResult.ok) return err(responseResult.error); + const response = responseResult.value; if (response.error !== undefined) { return err({ kind: MDBErrorKind.Api, @@ -179,6 +178,7 @@ export class OpenLibraryAPI extends APIModel { year: result.first_publish_year?.toString() ?? 'unknown', dataSource: this.apiName, id: result.key ?? title, + url: result.key ? `https://openlibrary.org${result.key}` : undefined, author: result.author_name?.join(', '), plot: this.pickDescription(result.description), genres: result.subject, @@ -187,6 +187,12 @@ export class OpenLibraryAPI extends APIModel { isbn: isbn10 ? Number(isbn10) : undefined, isbn13: isbn13 ? Number(isbn13) : undefined, image: result.cover_i ? `https://covers.openlibrary.org/b/id/${result.cover_i}-L.jpg` : undefined, + released: true, + userData: { + read: false, + lastRead: '', + personalRating: 0, + }, }), ); } @@ -215,9 +221,8 @@ export class OpenLibraryAPI extends APIModel { if (!bookResult.ok) return err(bookResult.error); const book = bookResult.value; - const olid = bookKey.replace(/^\/books\//i, ''); + const olid = bookKey.replace(/^\/books\//i, '').replace(/^\/isbn\//i, ''); const searchResult = await this.searchByOlid(olid); - const search = searchResult.ok ? searchResult.value : undefined; const title = book.title ?? search?.title ?? 'unknown'; @@ -227,8 +232,15 @@ export class OpenLibraryAPI extends APIModel { const yearFromSearch = search?.first_publish_year?.toString(); const year = yearFromBook ?? yearFromSearch ?? 'unknown'; - const pagesFromBook = book.pagination ? Number(book.pagination) : book.number_of_pages; - const pages = Number.isFinite(pagesFromBook!) ? Number(pagesFromBook) : (search?.number_of_pages_median ?? search?.number_of_pages); + const bookPagesRaw = book.number_of_pages ?? (book.pagination ? Number(book.pagination) : undefined); + const searchPagesRaw = search?.number_of_pages ?? search?.number_of_pages_median; + + const pages = + Number.isFinite(bookPagesRaw!) && Number(bookPagesRaw) > 0 + ? Number(bookPagesRaw) + : Number.isFinite(searchPagesRaw!) && Number(searchPagesRaw) > 0 + ? Number(searchPagesRaw) + : undefined; const bookIsbn10 = book.isbn_10?.find(el => el.length <= 10); const bookIsbn13 = book.isbn_13?.find(el => el.length === 13); @@ -248,7 +260,7 @@ export class OpenLibraryAPI extends APIModel { author: search?.author_name?.join(', '), plot: this.pickDescription(search?.description), genres: search?.subject, - pages: Number.isFinite(pages!) ? Number(pages) : undefined, + pages, image: coverId ? `https://covers.openlibrary.org/b/id/${coverId}-L.jpg` : undefined, released: true, userData: { @@ -293,8 +305,8 @@ export class OpenLibraryAPI extends APIModel { } const data = response.data as { docs: SearchResponse[] }; - const result = data.docs?.[0]; + if (!result) { return err({ kind: MDBErrorKind.Api, @@ -332,6 +344,7 @@ export class OpenLibraryAPI extends APIModel { }), ); } + getDisabledMediaTypes(): MediaType[] { return this.plugin.settings.OpenLibraryAPI_disabledMediaTypes; } From 979ed6804f25aaea9ac71d7e1d0bfc1286d24f82 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:08:47 +0200 Subject: [PATCH 5/6] forgot to include ratings for editions --- packages/obsidian/src/api/apis/OpenLibraryAPI.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts index e1f6d6c3..32ce14c4 100644 --- a/packages/obsidian/src/api/apis/OpenLibraryAPI.ts +++ b/packages/obsidian/src/api/apis/OpenLibraryAPI.ts @@ -261,6 +261,7 @@ export class OpenLibraryAPI extends APIModel { plot: this.pickDescription(search?.description), genres: search?.subject, pages, + onlineRating: search?.ratings_average, image: coverId ? `https://covers.openlibrary.org/b/id/${coverId}-L.jpg` : undefined, released: true, userData: { From 37b8057eddd0011b04435e8188db0ccea7bbb4b1 Mon Sep 17 00:00:00 2001 From: ltctceplrm <14954927+ltctceplrm@users.noreply.github.com> Date: Fri, 12 Jun 2026 02:04:25 +0200 Subject: [PATCH 6/6] fix crlf problem --- .github/workflows/checkPR.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checkPR.yml b/.github/workflows/checkPR.yml index 96fb0463..82fd5b70 100644 --- a/.github/workflows/checkPR.yml +++ b/.github/workflows/checkPR.yml @@ -22,4 +22,4 @@ jobs: - name: Run Checks run: | - bun run check \ No newline at end of file + bun run check