@zonuexe/techbook-mcp 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/README.md +54 -22
  3. package/dist/adapters/http/fetch-client.d.ts.map +1 -1
  4. package/dist/adapters/http/fetch-client.js +18 -1
  5. package/dist/adapters/http/fetch-client.js.map +1 -1
  6. package/dist/adapters/openbd.d.ts.map +1 -1
  7. package/dist/adapters/openbd.js +18 -5
  8. package/dist/adapters/openbd.js.map +1 -1
  9. package/dist/adapters/publishers/base.d.ts +2 -1
  10. package/dist/adapters/publishers/base.d.ts.map +1 -1
  11. package/dist/adapters/publishers/base.js +8 -3
  12. package/dist/adapters/publishers/base.js.map +1 -1
  13. package/dist/adapters/publishers/cq-publishing.d.ts +3 -0
  14. package/dist/adapters/publishers/cq-publishing.d.ts.map +1 -0
  15. package/dist/adapters/publishers/cq-publishing.js +120 -0
  16. package/dist/adapters/publishers/cq-publishing.js.map +1 -0
  17. package/dist/adapters/publishers/google-books.d.ts +4 -0
  18. package/dist/adapters/publishers/google-books.d.ts.map +1 -0
  19. package/dist/adapters/publishers/google-books.js +76 -0
  20. package/dist/adapters/publishers/google-books.js.map +1 -0
  21. package/dist/adapters/publishers/isbn-publisher-codes.d.ts +21 -0
  22. package/dist/adapters/publishers/isbn-publisher-codes.d.ts.map +1 -0
  23. package/dist/adapters/publishers/isbn-publisher-codes.js +49 -0
  24. package/dist/adapters/publishers/isbn-publisher-codes.js.map +1 -0
  25. package/dist/adapters/publishers/juse-p.d.ts +3 -0
  26. package/dist/adapters/publishers/juse-p.d.ts.map +1 -0
  27. package/dist/adapters/publishers/juse-p.js +110 -0
  28. package/dist/adapters/publishers/juse-p.js.map +1 -0
  29. package/dist/adapters/publishers/leanpub.d.ts +3 -0
  30. package/dist/adapters/publishers/leanpub.d.ts.map +1 -0
  31. package/dist/adapters/publishers/leanpub.js +96 -0
  32. package/dist/adapters/publishers/leanpub.js.map +1 -0
  33. package/dist/adapters/publishers/oreilly-japan.d.ts.map +1 -1
  34. package/dist/adapters/publishers/oreilly-japan.js +8 -2
  35. package/dist/adapters/publishers/oreilly-japan.js.map +1 -1
  36. package/dist/adapters/publishers/peaks.d.ts.map +1 -1
  37. package/dist/adapters/publishers/peaks.js +3 -2
  38. package/dist/adapters/publishers/peaks.js.map +1 -1
  39. package/dist/adapters/publishers/personal-media.d.ts.map +1 -1
  40. package/dist/adapters/publishers/personal-media.js +3 -2
  41. package/dist/adapters/publishers/personal-media.js.map +1 -1
  42. package/dist/adapters/publishers/pragprog.d.ts +3 -0
  43. package/dist/adapters/publishers/pragprog.d.ts.map +1 -0
  44. package/dist/adapters/publishers/pragprog.js +120 -0
  45. package/dist/adapters/publishers/pragprog.js.map +1 -0
  46. package/dist/adapters/publishers/registry.d.ts.map +1 -1
  47. package/dist/adapters/publishers/registry.js +10 -0
  48. package/dist/adapters/publishers/registry.js.map +1 -1
  49. package/dist/adapters/publishers/techbookfest.d.ts.map +1 -1
  50. package/dist/adapters/publishers/techbookfest.js +2 -1
  51. package/dist/adapters/publishers/techbookfest.js.map +1 -1
  52. package/dist/application/concurrency.d.ts +16 -0
  53. package/dist/application/concurrency.d.ts.map +1 -0
  54. package/dist/application/concurrency.js +42 -0
  55. package/dist/application/concurrency.js.map +1 -0
  56. package/dist/application/get-book-by-isbn.d.ts +0 -8
  57. package/dist/application/get-book-by-isbn.d.ts.map +1 -1
  58. package/dist/application/get-book-by-isbn.js +64 -7
  59. package/dist/application/get-book-by-isbn.js.map +1 -1
  60. package/dist/application/get-book-detail.d.ts.map +1 -1
  61. package/dist/application/get-book-detail.js +3 -0
  62. package/dist/application/get-book-detail.js.map +1 -1
  63. package/dist/application/search-books.d.ts +16 -5
  64. package/dist/application/search-books.d.ts.map +1 -1
  65. package/dist/application/search-books.js +46 -9
  66. package/dist/application/search-books.js.map +1 -1
  67. package/dist/config/credentials.d.ts +8 -0
  68. package/dist/config/credentials.d.ts.map +1 -0
  69. package/dist/config/credentials.js +32 -0
  70. package/dist/config/credentials.js.map +1 -0
  71. package/dist/domain/authors.d.ts +7 -0
  72. package/dist/domain/authors.d.ts.map +1 -0
  73. package/dist/domain/authors.js +22 -0
  74. package/dist/domain/authors.js.map +1 -0
  75. package/dist/domain/book.d.ts +2 -0
  76. package/dist/domain/book.d.ts.map +1 -1
  77. package/dist/domain/isbn.d.ts +8 -0
  78. package/dist/domain/isbn.d.ts.map +1 -0
  79. package/dist/domain/isbn.js +16 -0
  80. package/dist/domain/isbn.js.map +1 -0
  81. package/dist/domain/publisher.d.ts +16 -0
  82. package/dist/domain/publisher.d.ts.map +1 -1
  83. package/dist/domain/text-match.d.ts +32 -0
  84. package/dist/domain/text-match.d.ts.map +1 -0
  85. package/dist/domain/text-match.js +84 -0
  86. package/dist/domain/text-match.js.map +1 -0
  87. package/dist/main.js +15 -1
  88. package/dist/main.js.map +1 -1
  89. package/dist/mcp/server.d.ts +6 -0
  90. package/dist/mcp/server.d.ts.map +1 -1
  91. package/dist/mcp/server.js +40 -4
  92. package/dist/mcp/server.js.map +1 -1
  93. package/dist/mcp/tools.d.ts.map +1 -1
  94. package/dist/mcp/tools.js +9 -1
  95. package/dist/mcp/tools.js.map +1 -1
  96. package/dist/setup.d.ts +2 -0
  97. package/dist/setup.d.ts.map +1 -0
  98. package/dist/setup.js +43 -0
  99. package/dist/setup.js.map +1 -0
  100. package/dist/version.d.ts +9 -0
  101. package/dist/version.d.ts.map +1 -0
  102. package/dist/version.js +9 -0
  103. package/dist/version.js.map +1 -0
  104. package/docs/design-doc.md +127 -7
  105. package/package.json +14 -15
  106. package/.claude/settings.local.json +0 -38
  107. package/.codex/skills/techbook-mcp-release-prep/SKILL.md +0 -105
  108. package/.github/workflows/test.yml +0 -72
  109. package/.oxlintrc.json +0 -12
  110. package/AGENTS.md +0 -100
  111. package/deno.json +0 -3
  112. package/src/adapters/cache/memory-cache.ts +0 -31
  113. package/src/adapters/cache/null-cache.ts +0 -8
  114. package/src/adapters/calil.ts +0 -57
  115. package/src/adapters/html/cheerio-parser.ts +0 -50
  116. package/src/adapters/http/fetch-client.ts +0 -47
  117. package/src/adapters/http/mock-client.ts +0 -77
  118. package/src/adapters/openbd.ts +0 -142
  119. package/src/adapters/publishers/base.ts +0 -279
  120. package/src/adapters/publishers/book-tech.ts +0 -117
  121. package/src/adapters/publishers/born-digital.ts +0 -143
  122. package/src/adapters/publishers/coronasha.ts +0 -139
  123. package/src/adapters/publishers/gihyo.ts +0 -120
  124. package/src/adapters/publishers/impress.ts +0 -103
  125. package/src/adapters/publishers/lambdanote.ts +0 -146
  126. package/src/adapters/publishers/manatee.ts +0 -113
  127. package/src/adapters/publishers/maruzen-publishing.ts +0 -129
  128. package/src/adapters/publishers/optronics.ts +0 -113
  129. package/src/adapters/publishers/oreilly-japan.ts +0 -133
  130. package/src/adapters/publishers/peaks.ts +0 -98
  131. package/src/adapters/publishers/personal-media.ts +0 -168
  132. package/src/adapters/publishers/registry.ts +0 -38
  133. package/src/adapters/publishers/rutles.ts +0 -149
  134. package/src/adapters/publishers/saiensu.ts +0 -136
  135. package/src/adapters/publishers/seshop.ts +0 -121
  136. package/src/adapters/publishers/tatsu-zine.ts +0 -142
  137. package/src/adapters/publishers/techbookfest.ts +0 -179
  138. package/src/application/get-book-by-isbn.ts +0 -50
  139. package/src/application/get-book-detail.ts +0 -40
  140. package/src/application/search-books.ts +0 -64
  141. package/src/domain/book.ts +0 -35
  142. package/src/domain/publisher.ts +0 -18
  143. package/src/main.ts +0 -14
  144. package/src/mcp/server.ts +0 -113
  145. package/src/mcp/tools.ts +0 -71
  146. package/src/ports/cache.ts +0 -5
  147. package/src/ports/html-parser.ts +0 -15
  148. package/src/ports/http.ts +0 -17
  149. package/tests/fixtures/book-tech-detail.html +0 -51
  150. package/tests/fixtures/book-tech-search.html +0 -91
  151. package/tests/fixtures/born-digital-detail.html +0 -62
  152. package/tests/fixtures/born-digital-search.html +0 -51
  153. package/tests/fixtures/calil-book.html +0 -987
  154. package/tests/fixtures/coronasha-detail.html +0 -41
  155. package/tests/fixtures/coronasha-search.html +0 -61
  156. package/tests/fixtures/gihyo-detail.html +0 -42
  157. package/tests/fixtures/gihyo-search.json +0 -54
  158. package/tests/fixtures/impress-detail-epub.html +0 -746
  159. package/tests/fixtures/impress-detail-social.html +0 -689
  160. package/tests/fixtures/lambdanote-search.html +0 -66
  161. package/tests/fixtures/manatee-detail.html +0 -53
  162. package/tests/fixtures/manatee-search.html +0 -59
  163. package/tests/fixtures/maruzen-detail.html +0 -51
  164. package/tests/fixtures/maruzen-search.html +0 -60
  165. package/tests/fixtures/openbd-response.json +0 -110
  166. package/tests/fixtures/optronics-detail.html +0 -30
  167. package/tests/fixtures/optronics-search.html +0 -75
  168. package/tests/fixtures/oreilly-detail.html +0 -52
  169. package/tests/fixtures/oreilly-ebook-list.html +0 -53
  170. package/tests/fixtures/peaks-detail.html +0 -39
  171. package/tests/fixtures/peaks-top.html +0 -50
  172. package/tests/fixtures/personal-media-detail.html +0 -32
  173. package/tests/fixtures/personal-media-search.html +0 -39
  174. package/tests/fixtures/rutles-detail.html +0 -32
  175. package/tests/fixtures/rutles-search.html +0 -62
  176. package/tests/fixtures/saiensu-detail.html +0 -41
  177. package/tests/fixtures/saiensu-search.html +0 -65
  178. package/tests/fixtures/seshop-detail.html +0 -45
  179. package/tests/fixtures/seshop-search.html +0 -58
  180. package/tests/fixtures/tatsu-zine-detail-free.html +0 -24
  181. package/tests/fixtures/tatsu-zine-search.html +0 -40
  182. package/tests/fixtures/techbookfest-search.json +0 -73
  183. package/tests/unit/adapters/base.test.ts +0 -441
  184. package/tests/unit/adapters/calil.test.ts +0 -69
  185. package/tests/unit/adapters/openbd.test.ts +0 -185
  186. package/tests/unit/adapters/publishers/book-tech.test.ts +0 -186
  187. package/tests/unit/adapters/publishers/born-digital.test.ts +0 -194
  188. package/tests/unit/adapters/publishers/coronasha.test.ts +0 -207
  189. package/tests/unit/adapters/publishers/gihyo.test.ts +0 -137
  190. package/tests/unit/adapters/publishers/impress.test.ts +0 -129
  191. package/tests/unit/adapters/publishers/lambdanote.test.ts +0 -85
  192. package/tests/unit/adapters/publishers/manatee.test.ts +0 -165
  193. package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +0 -179
  194. package/tests/unit/adapters/publishers/optronics.test.ts +0 -208
  195. package/tests/unit/adapters/publishers/oreilly-japan.test.ts +0 -194
  196. package/tests/unit/adapters/publishers/peaks.test.ts +0 -177
  197. package/tests/unit/adapters/publishers/personal-media.test.ts +0 -199
  198. package/tests/unit/adapters/publishers/rutles.test.ts +0 -173
  199. package/tests/unit/adapters/publishers/saiensu.test.ts +0 -169
  200. package/tests/unit/adapters/publishers/seshop.test.ts +0 -174
  201. package/tests/unit/adapters/publishers/tatsu-zine.test.ts +0 -172
  202. package/tests/unit/adapters/publishers/techbookfest.test.ts +0 -94
  203. package/tests/unit/adapters/registry.test.ts +0 -37
  204. package/tests/unit/application/get-book-by-isbn.test.ts +0 -176
  205. package/tests/unit/application/get-book-detail.test.ts +0 -102
  206. package/tests/unit/application/search-books.test.ts +0 -137
  207. package/tsconfig.json +0 -17
@@ -1,146 +0,0 @@
1
- import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
2
- import type { BookRecord, SearchQuery } from "../../domain/book.js";
3
- import { fetchText, parseJapanesePrice, resolveUrl, extractAsin, extractEbookStoresFromDoc } from "./base.js";
4
- import type { EbookStore } from "../../domain/book.js";
5
-
6
- const BASE_URL = "https://www.lambdanote.com";
7
-
8
- function extractIsbn(text: string): string | undefined {
9
- const match = text.match(/97[89]-[\d-]{10,}/);
10
- return match ? match[0].replace(/-/g, "") : undefined;
11
- }
12
-
13
- /**
14
- * Shopify ページに埋め込まれた product JSON を取得する。
15
- * <script type="application/json" id="ProductJson-..."> または
16
- * data-product-json 属性を探す。
17
- */
18
- function parseShopifyProductJson(html: string): Record<string, unknown> | null {
19
- const match = html.match(
20
- /<script[^>]+type=["']application\/json["'][^>]*id=["']ProductJson[^"']*["'][^>]*>([\s\S]*?)<\/script>/i,
21
- );
22
- if (match?.[1]) {
23
- try {
24
- return JSON.parse(match[1]) as Record<string, unknown>;
25
- } catch {
26
- // fall through
27
- }
28
- }
29
- return null;
30
- }
31
-
32
- export const lambdanoteAdapter: PublisherAdapter = {
33
- id: "lambdanote",
34
- name: "ラムダノート",
35
- baseUrl: BASE_URL,
36
-
37
- async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
38
- const word = [query.title, query.author].filter(Boolean).join(" ");
39
- if (!word) return [];
40
-
41
- const url = `${BASE_URL}/search?q=${encodeURIComponent(word)}&type=product`;
42
- const html = await fetchText(url, deps);
43
- const doc = deps.parser.parse(html);
44
-
45
- const results: BookRecord[] = [];
46
-
47
- // Shopify Dawn テーマ系の検索結果セレクター
48
- const items = doc.select(
49
- ".search-results__list-item, li.search-result-product",
50
- );
51
-
52
- for (const item of items) {
53
- const titleEl =
54
- item.find(".card__heading a")[0] ??
55
- item.find("h2 a")[0] ??
56
- item.find("h3 a")[0] ??
57
- item.find("a.full-unstyled-link")[0];
58
-
59
- if (!titleEl) continue;
60
-
61
- const title = titleEl.text();
62
- const href = titleEl.attr("href");
63
- if (!title || !href) continue;
64
-
65
- const bookUrl = resolveUrl(BASE_URL, href);
66
-
67
- const priceEl =
68
- item.find(".price__regular .price-item")[0] ??
69
- item.find(".price-item")[0] ??
70
- item.find(".price")[0];
71
- const price = priceEl ? parseJapanesePrice(priceEl.text()) : undefined;
72
-
73
- const imgEl = item.find("img")[0];
74
- const imgSrc = imgEl?.attr("src") ?? imgEl?.attr("data-src");
75
- const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
76
-
77
- results.push({
78
- title,
79
- authors: [],
80
- publisher: "ラムダノート",
81
- url: bookUrl,
82
- price,
83
- coverImageUrl,
84
- });
85
- }
86
-
87
- return results.slice(0, query.limit ?? 10);
88
- },
89
-
90
- async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
91
- const html = await fetchText(url, deps);
92
- const doc = deps.parser.parse(html);
93
-
94
- const title =
95
- doc.selectOne("h1.product__title")?.text() ??
96
- doc.selectOne("h1")?.text() ??
97
- "";
98
-
99
- const priceEl =
100
- doc.selectOne(".price__regular .price-item") ??
101
- doc.selectOne(".price-item--regular") ??
102
- doc.selectOne("[class*='price']");
103
- const price = priceEl ? parseJapanesePrice(priceEl.text()) : undefined;
104
-
105
- const descEl =
106
- doc.selectOne(".product__description") ??
107
- doc.selectOne("[class*='description']");
108
- const description = descEl?.text() || undefined;
109
-
110
- const isbn = extractIsbn(html);
111
- const asin = extractAsin(html);
112
-
113
- // vendor フィールドに著者が入っている場合がある
114
- const productJson = parseShopifyProductJson(html);
115
- const vendor = typeof productJson?.["vendor"] === "string" ? productJson["vendor"] : null;
116
- const authors: string[] = vendor
117
- ? vendor.split(/[,、//]/).map((a: string) => a.trim()).filter(Boolean)
118
- : [];
119
-
120
- const imgEl =
121
- doc.selectOne(".product__media img") ??
122
- doc.selectOne(".product-featured-image") ??
123
- doc.selectOne("[class*='product'] img");
124
- const imgSrc = imgEl?.attr("src") ?? imgEl?.attr("data-src");
125
- const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
126
-
127
- // ラムダノートは購入時生成の一意IDをPDF欄外に印字 (ソーシャルDRM)
128
- const ebookStores: EbookStore[] = [
129
- { name: "ラムダノート", url, drm: "social" },
130
- ...extractEbookStoresFromDoc(doc).filter(s => s.name !== "ラムダノート"),
131
- ];
132
-
133
- return {
134
- title,
135
- authors,
136
- publisher: "ラムダノート",
137
- url,
138
- price,
139
- description,
140
- isbn,
141
- asin,
142
- coverImageUrl,
143
- ebookStores,
144
- };
145
- },
146
- };
@@ -1,113 +0,0 @@
1
- import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
2
- import type { BookRecord, SearchQuery } from "../../domain/book.js";
3
- import type { HtmlDocument } from "../../ports/html-parser.js";
4
- import { fetchText, parseJapanesePrice, resolveUrl } from "./base.js";
5
-
6
- const BASE_URL = "https://book.mynavi.jp/manatee";
7
- const BOOKS_URL = `${BASE_URL}/books/`;
8
-
9
- /**
10
- * `.attribute li` 内の著者リンクを配列にする。
11
- * 各 <a> のテキストが著者名(役割は括弧内テキストで付記されているが名前は <a> 内)。
12
- */
13
- function parseAuthors(doc: HtmlDocument): string[] {
14
- return doc
15
- .select(".attribute li a")
16
- .map(el => el.text().trim())
17
- .filter(Boolean);
18
- }
19
-
20
- export const manateeAdapter: PublisherAdapter = {
21
- id: "manatee",
22
- name: "マナティ",
23
- baseUrl: BASE_URL,
24
-
25
- async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
26
- const word = [query.title, query.author].filter(Boolean).join(" ");
27
- if (!word) return [];
28
-
29
- const url = `${BOOKS_URL}?topics_keyword=${encodeURIComponent(word)}`;
30
- const html = await fetchText(url, deps);
31
- const doc = deps.parser.parse(html);
32
-
33
- const results: BookRecord[] = [];
34
- const limit = query.limit ?? 10;
35
-
36
- // 書籍アイテムは <!-- item --> コメントで区切られた <li> 内
37
- for (const li of doc.select("ul.category_list li")) {
38
- const titleEl = li.find("dl.detail dt a")[0];
39
- if (!titleEl) continue;
40
-
41
- const title = titleEl.text().trim();
42
- if (!title) continue;
43
-
44
- const href = titleEl.attr("href");
45
- if (!href) continue;
46
- const bookUrl = href.startsWith("http") ? href : resolveUrl(BASE_URL, href);
47
-
48
- const priceText = li.find(".detail__price")[0]?.text().trim();
49
- const price = priceText ? parseJapanesePrice(priceText) : undefined;
50
-
51
- const imgEl = li.find(".image img")[0];
52
- const imgSrc = imgEl?.attr("src");
53
- const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
54
-
55
- results.push({
56
- title,
57
- authors: [],
58
- publisher: "マナティ",
59
- url: bookUrl,
60
- price,
61
- coverImageUrl,
62
- ebookStores: [{ name: "マナティ", url: bookUrl, drm: "social" }],
63
- });
64
-
65
- if (results.length >= limit) break;
66
- }
67
-
68
- return results;
69
- },
70
-
71
- async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
72
- const html = await fetchText(url, deps);
73
- const doc = deps.parser.parse(html);
74
-
75
- const title = doc.selectOne("h1.title")?.text().trim() ?? "";
76
-
77
- // `.intro` の最初の <p> が出版社名
78
- const publisher = doc.selectOne(".intro p")?.text().trim() ?? "マナティ";
79
-
80
- const authors = parseAuthors(doc);
81
-
82
- // 購入形態テーブルの最初の行から価格取得
83
- const priceText = doc.selectOne("#item_selectlist table .price")?.text().trim();
84
- const price = priceText ? parseJapanesePrice(priceText) : undefined;
85
-
86
- // 表紙画像の alt 属性が "ISBN.jpg" 形式(例: "9784839984274.jpg")
87
- const imgEl = doc.selectOne("div.item_meta_image img");
88
- const imgSrc = imgEl?.attr("src");
89
- const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
90
- const imgAlt = imgEl?.attr("alt") ?? "";
91
- const isbn = imgAlt.replace(/\.jpg$/i, "").replace(/-/g, "") || undefined;
92
-
93
- // 発売日: "発売日:2024-03-22"
94
- const dateText = doc.selectOne("p.date")?.text().trim();
95
- const publishedAt = dateText?.match(/(\d{4}-\d{2}-\d{2})/)?.[1] ?? undefined;
96
-
97
- const descEl = doc.selectOne(".item_desc p:not(.date):not(.pages)");
98
- const description = descEl?.text().trim() || undefined;
99
-
100
- return {
101
- title,
102
- authors,
103
- publisher,
104
- url,
105
- isbn,
106
- price,
107
- publishedAt,
108
- description,
109
- coverImageUrl,
110
- ebookStores: [{ name: "マナティ", url, drm: "social" }],
111
- };
112
- },
113
- };
@@ -1,129 +0,0 @@
1
- import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
2
- import type { BookRecord, SearchQuery } from "../../domain/book.js";
3
- import { fetchText, resolveUrl, extractEbookStoresFromDoc, parseJapaneseDateToISO, stripAuthorRole } from "./base.js";
4
-
5
- const BASE_URL = "https://www.maruzen-publishing.co.jp";
6
- const SEARCH_URL = `${BASE_URL}/search/`;
7
-
8
- /** 403回避のために自サイトをRefererとして付与する必要がある */
9
- const EXTRA_HEADERS = { Referer: `${BASE_URL}/` };
10
-
11
- /**
12
- * "ボリス・チェルニー 著 折山文哉 訳" → ["ボリス・チェルニー", "折山文哉"]
13
- * 役割語(著・訳・編・監訳・監修など)を除去する。
14
- */
15
- function parseAuthorsFromText(text: string): string[] {
16
- return text.split(/[ \s]+(?=\S)/).map(stripAuthorRole).filter(Boolean);
17
- }
18
-
19
- /**
20
- * div.author 内の各リンクから役割語を除去して著者名のみ返す。
21
- */
22
- function parseAuthorLinks(authors: string[]): string[] {
23
- return authors.map(stripAuthorRole).filter(Boolean);
24
- }
25
-
26
- /**
27
- * "2020/03/31" → "2020-03-31"
28
- * "2020年3月31日" → "2020-03-31"
29
- */
30
- function parseDate(text: string): string | undefined {
31
- const m1 = text.match(/(\d{4})\/(\d{1,2})\/(\d{1,2})/);
32
- if (m1) {
33
- return `${m1[1]}-${m1[2].padStart(2, "0")}-${m1[3].padStart(2, "0")}`;
34
- }
35
- return parseJapaneseDateToISO(text);
36
- }
37
-
38
- export const maruzenPublishingAdapter: PublisherAdapter = {
39
- id: "maruzen-publishing",
40
- name: "丸善出版",
41
- baseUrl: BASE_URL,
42
-
43
- async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
44
- const word = [query.title, query.author].filter(Boolean).join(" ");
45
- if (!word) return [];
46
-
47
- const url = `${SEARCH_URL}?search_keyword=${encodeURIComponent(word)}&format=1`;
48
- const html = await fetchText(url, deps, EXTRA_HEADERS);
49
- const doc = deps.parser.parse(html);
50
-
51
- const results: BookRecord[] = [];
52
- const limit = query.limit ?? 10;
53
-
54
- for (const item of doc.select("div.booklist div.item")) {
55
- const linkEl = item.find("div.ttl a")[0];
56
- if (!linkEl) continue;
57
-
58
- const title = linkEl.text().trim();
59
- const href = linkEl.attr("href");
60
- if (!title || !href) continue;
61
-
62
- const bookUrl = href.startsWith("http") ? href : `${BASE_URL}${href}`;
63
-
64
- const authorLinks = item.find("div.author a").map(el => el.text().trim());
65
- const authors = parseAuthorLinks(authorLinks);
66
-
67
- const imgEl = item.find("div.image img")[0];
68
- const imgSrc = imgEl?.attr("src");
69
- const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
70
-
71
- results.push({
72
- title,
73
- authors,
74
- publisher: "丸善出版",
75
- url: bookUrl,
76
- coverImageUrl,
77
- });
78
-
79
- if (results.length >= limit) break;
80
- }
81
-
82
- return results;
83
- },
84
-
85
- async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
86
- const html = await fetchText(url, deps, EXTRA_HEADERS);
87
- const doc = deps.parser.parse(html);
88
-
89
- // タイトルは <title> タグ、またはh1など(サイト構造に依存)
90
- // div#bookData table の th/td からメタデータを取得
91
- const table = doc.selectOne("div#bookData table");
92
- let authors: string[] = [];
93
- let publisher = "丸善出版";
94
- let publishedAt: string | undefined;
95
-
96
- if (table) {
97
- for (const row of table.find("tr")) {
98
- const th = row.find("th")[0]?.text().trim() ?? "";
99
- const tdText = row.find("td")[0]?.text().trim() ?? "";
100
-
101
- if (th === "著者") {
102
- authors = parseAuthorsFromText(tdText);
103
- } else if (th === "発行元") {
104
- publisher = tdText || "丸善出版";
105
- } else if (th === "出版年月日") {
106
- publishedAt = parseDate(tdText);
107
- }
108
- }
109
- }
110
-
111
- // ページタイトルから書籍タイトルを取得(" | 丸善出版" を除去)
112
- const pageTitle = doc.selectOne("title")?.text().trim() ?? "";
113
- const title = pageTitle.replace(/\s*[||]\s*丸善出版.*$/, "").trim();
114
-
115
- // 電子書籍ストア(kw.maruzen.co.jp は機関向けなので除外)
116
- const ebookStores = extractEbookStoresFromDoc(doc).filter(
117
- store => !store.url.includes("kw.maruzen.co.jp"),
118
- );
119
-
120
- return {
121
- title,
122
- authors,
123
- publisher,
124
- url,
125
- publishedAt,
126
- ebookStores: ebookStores.length > 0 ? ebookStores : undefined,
127
- };
128
- },
129
- };
@@ -1,113 +0,0 @@
1
- import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
2
- import type { BookRecord, SearchQuery } from "../../domain/book.js";
3
- import { fetchText, parseJapanesePrice, resolveUrl } from "./base.js";
4
-
5
- const BASE_URL = "https://optronics-ebook.com";
6
- const LIST_URL = `${BASE_URL}/products/list.php`;
7
-
8
- /** 電子書籍カテゴリID */
9
- const EBOOK_CATEGORY_ID = "1";
10
-
11
- /**
12
- * listcomment / main_comment のテキストから各フィールドを抽出する。
13
- * フォーマット例:
14
- * "著者:波多腰 玄一\n発行:㈱オプトロニクス社\n頁数:384頁\n..."
15
- */
16
- function parseComment(text: string): { authors: string[]; publisher?: string } {
17
- const authorMatch = text.match(/著者[::]\s*([^\n<]+)/);
18
- const authors = authorMatch
19
- ? authorMatch[1].split(/[、,]/).map(s => s.trim()).filter(Boolean)
20
- : [];
21
-
22
- const publisherMatch = text.match(/発行[::]\s*([^\n<]+)/);
23
- const publisher = publisherMatch
24
- ? publisherMatch[1].replace(/[㈱㈲]/g, "").trim() || undefined
25
- : undefined;
26
-
27
- return { authors, publisher };
28
- }
29
-
30
- export const optronicsAdapter: PublisherAdapter = {
31
- id: "optronics",
32
- name: "オプトロニクス社",
33
- baseUrl: BASE_URL,
34
-
35
- async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
36
- const word = [query.title, query.author].filter(Boolean).join(" ");
37
- if (!word) return [];
38
-
39
- const url = `${LIST_URL}?name=${encodeURIComponent(word)}&category_id=${EBOOK_CATEGORY_ID}`;
40
- const html = await fetchText(url, deps);
41
- const doc = deps.parser.parse(html);
42
-
43
- const results: BookRecord[] = [];
44
- const limit = query.limit ?? 10;
45
-
46
- for (const block of doc.select("div.list_area")) {
47
- const titleEl = block.find("div.listrightbloc h3 a")[0];
48
- if (!titleEl) continue;
49
-
50
- const title = titleEl.text().trim();
51
- const href = titleEl.attr("href");
52
- if (!title || !href) continue;
53
-
54
- const bookUrl = href.startsWith("http") ? href : `${BASE_URL}${href}`;
55
-
56
- const priceText = block.find("div.pricebox .price")[0]?.text().trim();
57
- const price = priceText ? parseJapanesePrice(priceText) : undefined;
58
-
59
- const commentText = block.find("div.listcomment")[0]?.text().trim() ?? "";
60
- const { authors, publisher } = parseComment(commentText);
61
-
62
- const imgEl = block.find("div.listphoto img.picture")[0];
63
- const imgSrc = imgEl?.attr("src");
64
- const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
65
-
66
- results.push({
67
- title,
68
- authors,
69
- publisher: publisher ?? "オプトロニクス社",
70
- url: bookUrl,
71
- price,
72
- coverImageUrl,
73
- ebookStores: [{ name: "オプトロニクス社", url: bookUrl, drm: "free" }],
74
- });
75
-
76
- if (results.length >= limit) break;
77
- }
78
-
79
- return results;
80
- },
81
-
82
- async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
83
- const html = await fetchText(url, deps);
84
- const doc = deps.parser.parse(html);
85
-
86
- // タイトル: <title>OPTRONICS eBOOK / {title}</title>
87
- const rawTitle = doc.selectOne("title")?.text().trim() ?? "";
88
- const title = rawTitle.replace(/^OPTRONICS\s+eBOOK\s*[//]\s*/, "").trim();
89
-
90
- // 価格: #price02_default
91
- const priceText = doc.selectOne("#price02_default")?.text().trim();
92
- const price = priceText ? parseJapanesePrice(priceText) : undefined;
93
-
94
- // 著者・発行元: div.main_comment テキスト
95
- const commentText = doc.selectOne("div.main_comment")?.text().trim() ?? "";
96
- const { authors, publisher } = parseComment(commentText);
97
-
98
- // カバー画像: img.picture
99
- const imgEl = doc.selectOne("img.picture");
100
- const imgSrc = imgEl?.attr("src");
101
- const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
102
-
103
- return {
104
- title,
105
- authors,
106
- publisher: publisher ?? "オプトロニクス社",
107
- url,
108
- price,
109
- coverImageUrl,
110
- ebookStores: [{ name: "オプトロニクス社", url, drm: "free" }],
111
- };
112
- },
113
- };
@@ -1,133 +0,0 @@
1
- import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
2
- import type { BookRecord, SearchQuery } from "../../domain/book.js";
3
- import { fetchText, parseJapanesePrice, resolveUrl, parseJapaneseDateToISO, stripAuthorRole } from "./base.js";
4
-
5
- const BASE_URL = "https://www.oreilly.co.jp";
6
- const EBOOK_LIST_URL = `${BASE_URL}/ebook/`;
7
-
8
- /**
9
- * "2026年04月03日" → "2026-04-03"
10
- * "2025-04-08" (content属性) はそのまま返す
11
- */
12
- function parseOreillyDate(text: string): string | undefined {
13
- const isoMatch = text.match(/\d{4}-\d{2}-\d{2}/);
14
- if (isoMatch) return isoMatch[0];
15
- return parseJapaneseDateToISO(text);
16
- }
17
-
18
- /**
19
- * 著者文字列をパースして配列に変換する。
20
- * 例: "Dan Vanderkam 著、今村 謙士 訳" → ["Dan Vanderkam", "今村 謙士"]
21
- */
22
- function parseAuthors(text: string): string[] {
23
- return text.split(/[、,]/).map(stripAuthorRole).filter(Boolean);
24
- }
25
-
26
- export const oreillyJapanAdapter: PublisherAdapter = {
27
- id: "oreilly-japan",
28
- name: "オライリー・ジャパン",
29
- baseUrl: BASE_URL,
30
-
31
- async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
32
- // 検索APIがないためタイトルでローカルフィルタリングする
33
- // 著者のみの検索は各書籍詳細ページを全取得しないと不可能なため非対応
34
- if (!query.title) return [];
35
-
36
- const keyword = query.title.toLowerCase();
37
- const limit = query.limit ?? 10;
38
-
39
- const html = await fetchText(EBOOK_LIST_URL, deps);
40
- const doc = deps.parser.parse(html);
41
-
42
- const results: BookRecord[] = [];
43
-
44
- for (const row of doc.select(".ebookCatalog tbody tr")) {
45
- const titleEl = row.find("td.title a")[0];
46
- if (!titleEl) continue;
47
-
48
- const title = titleEl.text().trim();
49
- if (!title.toLowerCase().includes(keyword)) continue;
50
-
51
- const href = titleEl.attr("href");
52
- if (!href) continue;
53
- const url = resolveUrl(EBOOK_LIST_URL, href);
54
-
55
- const isbnRaw = row.find("td.isbn")[0]?.text().trim();
56
- const isbn = isbnRaw?.replace(/-/g, "");
57
-
58
- const priceText = row.find("td.price")[0]?.text().trim();
59
- const price = priceText ? parseJapanesePrice(priceText) : undefined;
60
-
61
- const dateText = row.find("td")[3]?.text().trim();
62
- const publishedAt = dateText ? parseOreillyDate(dateText) : undefined;
63
-
64
- const coverImageUrl = isbn
65
- ? `${BASE_URL}/books/images/picture_large${isbnRaw}.jpeg`
66
- : undefined;
67
-
68
- results.push({
69
- title,
70
- authors: [],
71
- publisher: "オライリー・ジャパン",
72
- url,
73
- isbn,
74
- price,
75
- publishedAt,
76
- coverImageUrl,
77
- ebookStores: [{ name: "オライリー・ジャパン", url, drm: "free" }],
78
- });
79
-
80
- if (results.length >= limit) break;
81
- }
82
-
83
- return results;
84
- },
85
-
86
- async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
87
- const html = await fetchText(url, deps);
88
- const doc = deps.parser.parse(html);
89
-
90
- const titleMain = doc.selectOne("h1[itemprop='name']")?.text().trim() ?? "";
91
- const subTitle = doc.selectOne("p.sub_title")?.text()
92
- .replace(/^[\s\u3000―-]+/, "").trim();
93
- const title = subTitle ? `${titleMain} ―${subTitle}` : titleMain;
94
-
95
- const authorText = doc.selectOne("span[itemprop='author']")?.text().trim() ?? "";
96
- const authors = parseAuthors(authorText);
97
-
98
- const isbnRaw = doc.selectOne("dd[itemprop='isbn']")?.text().trim();
99
- const isbn = isbnRaw?.replace(/-/g, "");
100
-
101
- const publishedAt = doc.selectOne("dd[itemprop='datePublished']")?.attr("content")
102
- ?? undefined;
103
-
104
- const coverImageUrl = doc.selectOne("img.cover-photo")?.attr("src") ?? undefined;
105
-
106
- const description = doc.selectOne("p[itemprop='description']")?.text().trim()
107
- || undefined;
108
-
109
- // Ebook価格を取得: "Ebook" という option-name の次の div
110
- let price: number | undefined;
111
- for (const item of doc.select(".option-item")) {
112
- const name = item.find(".option-name")[0]?.text().trim();
113
- if (name === "Ebook") {
114
- const priceText = item.find("div")[1]?.text().trim();
115
- if (priceText) price = parseJapanesePrice(priceText);
116
- break;
117
- }
118
- }
119
-
120
- return {
121
- title,
122
- authors,
123
- publisher: "オライリー・ジャパン",
124
- url,
125
- isbn,
126
- price,
127
- publishedAt,
128
- description,
129
- coverImageUrl,
130
- ebookStores: [{ name: "オライリー・ジャパン", url, drm: "free" }],
131
- };
132
- },
133
- };