@zonuexe/techbook-mcp 0.2.3 → 0.2.4

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 (135) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/README.md +39 -20
  3. package/dist/adapters/publishers/google-books.d.ts +4 -0
  4. package/dist/adapters/publishers/google-books.d.ts.map +1 -0
  5. package/dist/adapters/publishers/google-books.js +75 -0
  6. package/dist/adapters/publishers/google-books.js.map +1 -0
  7. package/dist/adapters/publishers/isbn-publisher-codes.d.ts +21 -0
  8. package/dist/adapters/publishers/isbn-publisher-codes.d.ts.map +1 -0
  9. package/dist/adapters/publishers/isbn-publisher-codes.js +49 -0
  10. package/dist/adapters/publishers/isbn-publisher-codes.js.map +1 -0
  11. package/dist/adapters/publishers/juse-p.d.ts +3 -0
  12. package/dist/adapters/publishers/juse-p.d.ts.map +1 -0
  13. package/dist/adapters/publishers/juse-p.js +110 -0
  14. package/dist/adapters/publishers/juse-p.js.map +1 -0
  15. package/dist/adapters/publishers/registry.d.ts.map +1 -1
  16. package/dist/adapters/publishers/registry.js +4 -0
  17. package/dist/adapters/publishers/registry.js.map +1 -1
  18. package/dist/application/get-book-by-isbn.d.ts +3 -2
  19. package/dist/application/get-book-by-isbn.d.ts.map +1 -1
  20. package/dist/application/get-book-by-isbn.js +22 -3
  21. package/dist/application/get-book-by-isbn.js.map +1 -1
  22. package/dist/config/credentials.d.ts +8 -0
  23. package/dist/config/credentials.d.ts.map +1 -0
  24. package/dist/config/credentials.js +32 -0
  25. package/dist/config/credentials.js.map +1 -0
  26. package/dist/main.js +15 -1
  27. package/dist/main.js.map +1 -1
  28. package/dist/setup.d.ts +2 -0
  29. package/dist/setup.d.ts.map +1 -0
  30. package/dist/setup.js +43 -0
  31. package/dist/setup.js.map +1 -0
  32. package/flake.lock +61 -0
  33. package/package.json +1 -1
  34. package/.claude/settings.local.json +0 -38
  35. package/.codex/skills/techbook-mcp-release-prep/SKILL.md +0 -105
  36. package/.github/workflows/test.yml +0 -72
  37. package/.oxlintrc.json +0 -12
  38. package/AGENTS.md +0 -100
  39. package/deno.json +0 -3
  40. package/src/adapters/cache/memory-cache.ts +0 -31
  41. package/src/adapters/cache/null-cache.ts +0 -8
  42. package/src/adapters/calil.ts +0 -57
  43. package/src/adapters/html/cheerio-parser.ts +0 -50
  44. package/src/adapters/http/fetch-client.ts +0 -47
  45. package/src/adapters/http/mock-client.ts +0 -77
  46. package/src/adapters/openbd.ts +0 -142
  47. package/src/adapters/publishers/base.ts +0 -279
  48. package/src/adapters/publishers/book-tech.ts +0 -117
  49. package/src/adapters/publishers/born-digital.ts +0 -143
  50. package/src/adapters/publishers/coronasha.ts +0 -139
  51. package/src/adapters/publishers/gihyo.ts +0 -120
  52. package/src/adapters/publishers/impress.ts +0 -103
  53. package/src/adapters/publishers/lambdanote.ts +0 -146
  54. package/src/adapters/publishers/manatee.ts +0 -113
  55. package/src/adapters/publishers/maruzen-publishing.ts +0 -129
  56. package/src/adapters/publishers/optronics.ts +0 -113
  57. package/src/adapters/publishers/oreilly-japan.ts +0 -133
  58. package/src/adapters/publishers/peaks.ts +0 -98
  59. package/src/adapters/publishers/personal-media.ts +0 -168
  60. package/src/adapters/publishers/registry.ts +0 -38
  61. package/src/adapters/publishers/rutles.ts +0 -149
  62. package/src/adapters/publishers/saiensu.ts +0 -136
  63. package/src/adapters/publishers/seshop.ts +0 -121
  64. package/src/adapters/publishers/tatsu-zine.ts +0 -142
  65. package/src/adapters/publishers/techbookfest.ts +0 -179
  66. package/src/application/get-book-by-isbn.ts +0 -50
  67. package/src/application/get-book-detail.ts +0 -40
  68. package/src/application/search-books.ts +0 -64
  69. package/src/domain/book.ts +0 -35
  70. package/src/domain/publisher.ts +0 -18
  71. package/src/main.ts +0 -14
  72. package/src/mcp/server.ts +0 -113
  73. package/src/mcp/tools.ts +0 -71
  74. package/src/ports/cache.ts +0 -5
  75. package/src/ports/html-parser.ts +0 -15
  76. package/src/ports/http.ts +0 -17
  77. package/tests/fixtures/book-tech-detail.html +0 -51
  78. package/tests/fixtures/book-tech-search.html +0 -91
  79. package/tests/fixtures/born-digital-detail.html +0 -62
  80. package/tests/fixtures/born-digital-search.html +0 -51
  81. package/tests/fixtures/calil-book.html +0 -987
  82. package/tests/fixtures/coronasha-detail.html +0 -41
  83. package/tests/fixtures/coronasha-search.html +0 -61
  84. package/tests/fixtures/gihyo-detail.html +0 -42
  85. package/tests/fixtures/gihyo-search.json +0 -54
  86. package/tests/fixtures/impress-detail-epub.html +0 -746
  87. package/tests/fixtures/impress-detail-social.html +0 -689
  88. package/tests/fixtures/lambdanote-search.html +0 -66
  89. package/tests/fixtures/manatee-detail.html +0 -53
  90. package/tests/fixtures/manatee-search.html +0 -59
  91. package/tests/fixtures/maruzen-detail.html +0 -51
  92. package/tests/fixtures/maruzen-search.html +0 -60
  93. package/tests/fixtures/openbd-response.json +0 -110
  94. package/tests/fixtures/optronics-detail.html +0 -30
  95. package/tests/fixtures/optronics-search.html +0 -75
  96. package/tests/fixtures/oreilly-detail.html +0 -52
  97. package/tests/fixtures/oreilly-ebook-list.html +0 -53
  98. package/tests/fixtures/peaks-detail.html +0 -39
  99. package/tests/fixtures/peaks-top.html +0 -50
  100. package/tests/fixtures/personal-media-detail.html +0 -32
  101. package/tests/fixtures/personal-media-search.html +0 -39
  102. package/tests/fixtures/rutles-detail.html +0 -32
  103. package/tests/fixtures/rutles-search.html +0 -62
  104. package/tests/fixtures/saiensu-detail.html +0 -41
  105. package/tests/fixtures/saiensu-search.html +0 -65
  106. package/tests/fixtures/seshop-detail.html +0 -45
  107. package/tests/fixtures/seshop-search.html +0 -58
  108. package/tests/fixtures/tatsu-zine-detail-free.html +0 -24
  109. package/tests/fixtures/tatsu-zine-search.html +0 -40
  110. package/tests/fixtures/techbookfest-search.json +0 -73
  111. package/tests/unit/adapters/base.test.ts +0 -441
  112. package/tests/unit/adapters/calil.test.ts +0 -69
  113. package/tests/unit/adapters/openbd.test.ts +0 -185
  114. package/tests/unit/adapters/publishers/book-tech.test.ts +0 -186
  115. package/tests/unit/adapters/publishers/born-digital.test.ts +0 -194
  116. package/tests/unit/adapters/publishers/coronasha.test.ts +0 -207
  117. package/tests/unit/adapters/publishers/gihyo.test.ts +0 -137
  118. package/tests/unit/adapters/publishers/impress.test.ts +0 -129
  119. package/tests/unit/adapters/publishers/lambdanote.test.ts +0 -85
  120. package/tests/unit/adapters/publishers/manatee.test.ts +0 -165
  121. package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +0 -179
  122. package/tests/unit/adapters/publishers/optronics.test.ts +0 -208
  123. package/tests/unit/adapters/publishers/oreilly-japan.test.ts +0 -194
  124. package/tests/unit/adapters/publishers/peaks.test.ts +0 -177
  125. package/tests/unit/adapters/publishers/personal-media.test.ts +0 -199
  126. package/tests/unit/adapters/publishers/rutles.test.ts +0 -173
  127. package/tests/unit/adapters/publishers/saiensu.test.ts +0 -169
  128. package/tests/unit/adapters/publishers/seshop.test.ts +0 -174
  129. package/tests/unit/adapters/publishers/tatsu-zine.test.ts +0 -172
  130. package/tests/unit/adapters/publishers/techbookfest.test.ts +0 -94
  131. package/tests/unit/adapters/registry.test.ts +0 -37
  132. package/tests/unit/application/get-book-by-isbn.test.ts +0 -176
  133. package/tests/unit/application/get-book-detail.test.ts +0 -102
  134. package/tests/unit/application/search-books.test.ts +0 -137
  135. package/tsconfig.json +0 -17
@@ -1,143 +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, encodeEucJp, parseJapaneseDateToISO } from "./base.js";
4
-
5
- const BASE_URL = "https://wgn-obs.shop-pro.jp";
6
-
7
- /**
8
- * 商品説明テキストから著者・出版社・発売日を取得する。
9
- * 以下の2形式に対応:
10
- * - "著者\tヘイドン・ピカリング<br />" (タブ区切り)
11
- * - "著者:太田 良典、中村 直樹<br />" (全角コロン区切り)
12
- */
13
- function parseDescription(text: string): {
14
- authors: string[];
15
- publisher: string;
16
- publishedAt: string | undefined;
17
- } {
18
- const authors: string[] = [];
19
- let publisher = "ボーンデジタル";
20
- let publishedAt: string | undefined;
21
-
22
- for (const rawLine of text.split(/\r?\n/)) {
23
- const line = rawLine.trim();
24
- const m = line.match(/^(.+?)[\t:]\s*(.+)$/);
25
- if (!m) continue;
26
- const key = m[1].trim();
27
- const value = m[2].trim();
28
-
29
- if (key === "著者" || key === "著") {
30
- authors.push(...value.split(/[、,,]/).map(s => s.trim()).filter(Boolean));
31
- } else if (key === "翻訳" || key === "翻訳者" || key === "訳者" || key === "訳") {
32
- authors.push(...value.split(/[、,,]/).map(s => s.trim()).filter(Boolean));
33
- } else if (key.startsWith("発行") || key === "発売") {
34
- publisher = value.replace(/^株式会社\s*/, "").replace(/\s*株式会社$/, "").trim();
35
- } else if (key === "発売日") {
36
- publishedAt = parseJapaneseDateToISO(value);
37
- }
38
- }
39
-
40
- return { authors, publisher, publishedAt };
41
- }
42
-
43
- /**
44
- * ページ埋め込みの Colorme JSON から商品情報を取得する。
45
- * `var Colorme = {...};` 形式。
46
- */
47
- function extractColormeProduct(html: string): { price?: number } {
48
- const m = html.match(/var Colorme = (\{[^\n]+\});/);
49
- if (!m) return {};
50
- try {
51
- const data = JSON.parse(m[1]) as {
52
- product?: { sales_price_including_tax?: number };
53
- };
54
- const price = data.product?.sales_price_including_tax;
55
- return { price };
56
- } catch {
57
- return {};
58
- }
59
- }
60
-
61
- export const bornDigitalAdapter: PublisherAdapter = {
62
- id: "born-digital",
63
- name: "ボーンデジタル",
64
- baseUrl: BASE_URL,
65
-
66
- async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
67
- const word = [query.title, query.author].filter(Boolean).join(" ");
68
- if (!word) return [];
69
-
70
- const url = `${BASE_URL}/?mode=srh&keyword=${encodeEucJp(word)}`;
71
- const html = await fetchText(url, deps);
72
- const doc = deps.parser.parse(html);
73
-
74
- const results: BookRecord[] = [];
75
- const limit = query.limit ?? 10;
76
-
77
- for (const item of doc.select("li.c-product-list__item")) {
78
- const titleEl = item.find("a.c-product-list__name")[0];
79
- const title = titleEl?.text().trim();
80
- if (!title) continue;
81
-
82
- // 電子書籍のみ: タイトルが【電子書籍版】または【PDFダウンロード版】で始まる
83
- if (!title.startsWith("【")) continue;
84
-
85
- const href = item.find("a.c-product-list__image-wrap")[0]?.attr("href")
86
- ?? titleEl?.attr("href");
87
- if (!href) continue;
88
- const bookUrl = resolveUrl(BASE_URL + "/", href);
89
-
90
- const priceText = item.find(".c-product-list__price")[0]?.text();
91
- const price = priceText ? parseJapanesePrice(priceText) : undefined;
92
-
93
- const imgEl = item.find("a.c-product-list__image-wrap img.c-image-box__image")[0];
94
- const coverImageUrl = imgEl?.attr("src") ?? undefined;
95
-
96
- results.push({
97
- title,
98
- authors: [],
99
- publisher: "ボーンデジタル",
100
- url: bookUrl,
101
- price,
102
- coverImageUrl,
103
- ebookStores: [{ name: "ボーンデジタル", url: bookUrl, drm: "social" }],
104
- });
105
-
106
- if (results.length >= limit) break;
107
- }
108
-
109
- return results;
110
- },
111
-
112
- async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
113
- const html = await fetchText(url, deps);
114
- const doc = deps.parser.parse(html);
115
-
116
- // Colorme JSON から価格を取得
117
- const { price: colormePrice } = extractColormeProduct(html);
118
-
119
- const title = doc.selectOne(".p-cart-form__name")?.text().trim()
120
- ?? doc.selectOne(".p-product-body__name")?.text().trim()
121
- ?? "";
122
-
123
- const priceText = doc.selectOne(".c-product-info__price")?.text();
124
- const price = colormePrice ?? (priceText ? parseJapanesePrice(priceText) : undefined);
125
-
126
- const descText = doc.selectOne(".p-product-body__description")?.text() ?? "";
127
- const { authors, publisher, publishedAt } = parseDescription(descText);
128
-
129
- const imgEl = doc.selectOne(".p-large-image img");
130
- const coverImageUrl = imgEl?.attr("src") ?? undefined;
131
-
132
- return {
133
- title,
134
- authors,
135
- publisher,
136
- url,
137
- price,
138
- publishedAt,
139
- coverImageUrl,
140
- ebookStores: [{ name: "ボーンデジタル", url, drm: "social" }],
141
- };
142
- },
143
- };
@@ -1,139 +0,0 @@
1
- import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
2
- import type { BookRecord, SearchQuery } from "../../domain/book.js";
3
- import type { HtmlElement } from "../../ports/html-parser.js";
4
- import { fetchText, parseJapanesePrice, resolveUrl, extractEbookStoresFromDoc } from "./base.js";
5
-
6
- const BASE_URL = "https://www.coronasha.co.jp";
7
- const SEARCH_URL = `${BASE_URL}/np/result.html`;
8
-
9
- /** "2023/05/01" → "2023-05-01" */
10
- function parseDate(text: string): string | undefined {
11
- const m = text.match(/(\d{4})\/(\d{2})\/(\d{2})/);
12
- if (!m) return undefined;
13
- return `${m[1]}-${m[2]}-${m[3]}`;
14
- }
15
-
16
- /**
17
- * `.tunogaki` と `.book-title` からタイトルを組み立てる。
18
- * "1から始める" + "Juliaプログラミング" → "1から始める Juliaプログラミング"
19
- */
20
- function buildTitle(container: HtmlElement): string {
21
- const tunogaki = container.find(".tunogaki")[0]?.text().trim();
22
- const bookTitle = container.find(".book-title")[0]?.text().trim() ?? "";
23
- return tunogaki ? `${tunogaki} ${bookTitle}` : bookTitle;
24
- }
25
-
26
- /**
27
- * `dl` 要素の配列から `dt` をキー、`dd` をバリューとするマップを返す。
28
- * 価格・ISBN・発行年月日などの取得に使う。
29
- */
30
- function parseDlMap(dls: HtmlElement[]): Map<string, string> {
31
- const map = new Map<string, string>();
32
- for (const dl of dls) {
33
- const key = dl.find("dt")[0]?.text().trim();
34
- const val = dl.find("dd")[0]?.text().trim();
35
- if (key && val) map.set(key, val);
36
- }
37
- return map;
38
- }
39
-
40
- export const coronashaAdapter: PublisherAdapter = {
41
- id: "coronasha",
42
- name: "コロナ社",
43
- baseUrl: BASE_URL,
44
-
45
- async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
46
- const word = [query.title, query.author].filter(Boolean).join(" ");
47
- if (!word) return [];
48
-
49
- const url = `${SEARCH_URL}?q=${encodeURIComponent(word)}`;
50
- const html = await fetchText(url, deps);
51
- const doc = deps.parser.parse(html);
52
-
53
- const results: BookRecord[] = [];
54
- const limit = query.limit ?? 10;
55
-
56
- for (const item of doc.select("article.item")) {
57
- // 電子版があるものだけ対象
58
- const hasEbook = item.find("ul.status-list li")
59
- .some(el => el.text().trim() === "電子版あり");
60
- if (!hasEbook) continue;
61
-
62
- const linkEl = item.find("dl.col1 dt a")[0];
63
- const href = linkEl?.attr("href");
64
- if (!href) continue;
65
- const bookUrl = resolveUrl(BASE_URL, href);
66
-
67
- const title = buildTitle(item);
68
- if (!title) continue;
69
-
70
- const authors = item.find("ul.authors li a")
71
- .map(el => el.text().trim())
72
- .filter(Boolean);
73
-
74
- const info = parseDlMap(item.find(".book-info dl"));
75
- const price = parseJapanesePrice(info.get("定価") ?? "");
76
- const publishedAt = parseDate(info.get("発行年月日") ?? "");
77
- const isbn = info.get("ISBN")?.replace(/-/g, "") || undefined;
78
-
79
- const imgEl = item.find("img.cover")[0];
80
- const coverSrc = imgEl?.attr("src");
81
- const coverImageUrl = coverSrc ? resolveUrl(BASE_URL, coverSrc) : undefined;
82
-
83
- results.push({
84
- title,
85
- authors,
86
- publisher: "コロナ社",
87
- url: bookUrl,
88
- isbn,
89
- price,
90
- publishedAt,
91
- coverImageUrl,
92
- ebookStores: [],
93
- });
94
-
95
- if (results.length >= limit) break;
96
- }
97
-
98
- return results;
99
- },
100
-
101
- async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
102
- const html = await fetchText(url, deps);
103
- const doc = deps.parser.parse(html);
104
-
105
- const h2 = doc.selectOne("h2.title");
106
- const title = h2 ? buildTitle(h2) : "";
107
-
108
- const authors = doc.select("ul.authors li a")
109
- .map(el => el.text().trim())
110
- .filter(Boolean);
111
-
112
- const info = parseDlMap(doc.select(".book-info dl"));
113
- const publishedAt = parseDate(info.get("発行年月日") ?? "");
114
- const isbn = info.get("ISBN")?.replace(/-/g, "") || undefined;
115
-
116
- // 価格はサイドバーの .price に表示される(book-info dl には含まれない)
117
- const priceText = doc.selectOne(".price")?.text();
118
- const price = priceText ? parseJapanesePrice(priceText) : undefined;
119
-
120
- const imgEl = doc.selectOne("img.cover");
121
- const coverSrc = imgEl?.attr("src");
122
- const coverImageUrl = coverSrc ? resolveUrl(BASE_URL, coverSrc) : undefined;
123
-
124
- // 電子版購入ポップアップのリンクから電子書籍ストアを自動検出
125
- const ebookStores = extractEbookStoresFromDoc(doc);
126
-
127
- return {
128
- title,
129
- authors,
130
- publisher: "コロナ社",
131
- url,
132
- isbn,
133
- price,
134
- publishedAt,
135
- coverImageUrl,
136
- ebookStores,
137
- };
138
- },
139
- };
@@ -1,120 +0,0 @@
1
- import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
2
- import type { BookRecord, SearchQuery } from "../../domain/book.js";
3
- import { fetchText, stripHtmlTags, resolveUrl, extractAsin, extractEbookStoresFromDoc } from "./base.js";
4
-
5
- const BASE_URL = "https://gihyo.jp";
6
-
7
- // --- API レスポンス型 ---
8
-
9
- interface GihyoSearchResponse {
10
- lname: string;
11
- total: number;
12
- list: Record<string, GihyoBookEntry>;
13
- next: boolean;
14
- query: string;
15
- }
16
-
17
- interface GihyoBookEntry {
18
- series: string;
19
- title: string;
20
- subtitle: string;
21
- /** キー: 役割 ("著", "監修" など)、値: { 著者名: markup } */
22
- author: Record<string, Record<string, string>>;
23
- /** [定価, 割引価格] */
24
- price: [number, number];
25
- stock: number;
26
- /** ["YYYY.M.D", ""] */
27
- release: [string, string];
28
- /** 相対URL: "/book/YYYY/978-4-..." */
29
- url: string;
30
- upcoming: boolean;
31
- support: boolean;
32
- /** [thumb_url, width, height, full_url] */
33
- cover: [string, number, number, string];
34
- }
35
-
36
- // --- 変換ヘルパー ---
37
-
38
- function parseAuthors(authorField: Record<string, Record<string, string>>): string[] {
39
- return Object.values(authorField).flatMap(roleEntries =>
40
- Object.keys(roleEntries).map(name => stripHtmlTags(name).trim()),
41
- );
42
- }
43
-
44
- function parseReleaseDate(release: [string, string]): string | undefined {
45
- const raw = release[0];
46
- if (!raw) return undefined;
47
- // "2025.9.29" → "2025-09-29"
48
- const parts = raw.split(".");
49
- if (parts.length !== 3) return raw;
50
- const [y, m, d] = parts;
51
- return `${y}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`;
52
- }
53
-
54
- function entryToBookRecord(isbn: string, entry: GihyoBookEntry): BookRecord {
55
- const title = entry.subtitle
56
- ? `${entry.title} ${entry.subtitle}`
57
- : entry.title;
58
-
59
- return {
60
- title,
61
- authors: parseAuthors(entry.author),
62
- publisher: "技術評論社",
63
- publishedAt: parseReleaseDate(entry.release),
64
- isbn: isbn.replace(/-/g, ""),
65
- url: resolveUrl(BASE_URL, entry.url),
66
- price: entry.price[0] > 0 ? entry.price[0] : undefined,
67
- coverImageUrl: entry.cover[3] ? resolveUrl(BASE_URL, entry.cover[3]) : undefined,
68
- };
69
- }
70
-
71
- // --- アダプター実装 ---
72
-
73
- export const gihyoAdapter: PublisherAdapter = {
74
- id: "gihyo",
75
- name: "技術評論社",
76
- baseUrl: BASE_URL,
77
-
78
- async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
79
- const word = [query.title, query.author].filter(Boolean).join(" ");
80
- if (!word) return [];
81
-
82
- const limit = query.limit ?? 10;
83
- const url = `${BASE_URL}/api_gh/site/search?search=${encodeURIComponent(word)}&limit=${limit}`;
84
- const text = await fetchText(url, deps);
85
- const data: GihyoSearchResponse = JSON.parse(text);
86
-
87
- return Object.entries(data.list).map(([isbn, entry]) => entryToBookRecord(isbn, entry));
88
- },
89
-
90
- async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
91
- // URL例: https://gihyo.jp/book/2022/978-4-297-12815-2
92
- const isbnMatch = url.match(/\/(978-[\d-]+)\s*$/);
93
- if (!isbnMatch) throw new Error(`URLからISBNを取得できません: ${url}`);
94
-
95
- const isbn = isbnMatch[1];
96
- const apiUrl = `${BASE_URL}/api_gh/site/search?search=${encodeURIComponent(isbn)}&limit=1`;
97
-
98
- // JSONメタデータとebook store情報(HTML)を並列取得
99
- const [apiText, htmlText] = await Promise.all([
100
- fetchText(apiUrl, deps),
101
- fetchText(url, deps),
102
- ]);
103
-
104
- const data: GihyoSearchResponse = JSON.parse(apiText);
105
- const entry = data.list[isbn];
106
- if (!entry) throw new Error(`書籍が見つかりません: ${isbn}`);
107
-
108
- const base = entryToBookRecord(isbn, entry);
109
-
110
- const doc = deps.parser.parse(htmlText);
111
- const ebookStores = extractEbookStoresFromDoc(doc);
112
- const asin = extractAsin(htmlText);
113
-
114
- return {
115
- ...base,
116
- asin,
117
- ebookStores: ebookStores.length > 0 ? ebookStores : undefined,
118
- };
119
- },
120
- };
@@ -1,103 +0,0 @@
1
- import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
2
- import type { BookRecord, SearchQuery, DrmType } from "../../domain/book.js";
3
- import { fetchText, parseJapanesePrice, stripAuthorRole } from "./base.js";
4
-
5
- const BASE_URL = "https://book.impress.co.jp";
6
-
7
- /** "2026/1/22" → "2026-01-22" */
8
- function parseImpressDate(text: string): string | undefined {
9
- const m = text.trim().match(/(\d{4})\/(\d{1,2})\/(\d{1,2})/);
10
- if (!m) return undefined;
11
- return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
12
- }
13
-
14
- /**
15
- * 著者文字列から著者名配列を返す。
16
- * 例: "山本康太 著" → ["山本康太"]
17
- * 複数著者は「、」または改行で区切られる。
18
- */
19
- function parseAuthors(text: string): string[] {
20
- return text
21
- .split(/[、,\n]/)
22
- .map(s => stripAuthorRole(s.trim()))
23
- .filter(Boolean);
24
- }
25
-
26
- /**
27
- * 電子書籍ガイドのテキストから DRM 種別を判定する。
28
- * 明示されていない場合はインプレスの公式方針に基づき social を返す。
29
- */
30
- function parseDrmType(text: string): DrmType {
31
- if (/ソーシャルDRM/i.test(text)) return "social";
32
- if (/DRM-?free|DRMフリー/i.test(text)) return "free";
33
- if (/パスワード/i.test(text)) return "password_pdf";
34
- return "social";
35
- }
36
-
37
- export const impressBooksAdapter: PublisherAdapter = {
38
- id: "impress-books",
39
- name: "インプレスブックス",
40
- baseUrl: BASE_URL,
41
-
42
- async search(_query: SearchQuery, _deps: PublisherDeps): Promise<BookRecord[]> {
43
- // 検索ページは Google Custom Search Engine による JavaScript レンダリングのためスクレイピング不可
44
- return [];
45
- },
46
-
47
- async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
48
- const html = await fetchText(url, deps);
49
- const doc = deps.parser.parse(html);
50
-
51
- // タイトル(ページ内最初の h2)
52
- const title = doc.selectOne("h2")?.text().trim() ?? "";
53
-
54
- // dl.module-book-data の dt/dd を順序でペアリング
55
- const dts = doc.select("dl.module-book-data dt");
56
- const dds = doc.select("dl.module-book-data dd");
57
- const bookDataMap = new Map<string, string>();
58
- for (let i = 0; i < dts.length; i++) {
59
- const key = dts[i].text().trim();
60
- const val = dds[i]?.text().trim() ?? "";
61
- if (key) bookDataMap.set(key, val);
62
- }
63
-
64
- const authors = parseAuthors(bookDataMap.get("著者") ?? "");
65
- const isbn = bookDataMap.get("ISBN")?.replace(/\s/g, "") || undefined;
66
- const publishedAt = parseImpressDate(bookDataMap.get("発売日") ?? "");
67
-
68
- // カバー画像(img.ips.co.jp のプロトコル相対URLに https: を補完)
69
- const coverSrc = doc.selectOne(".block-book-detail-img img")?.attr("src");
70
- const coverImageUrl = coverSrc
71
- ? (coverSrc.startsWith("//") ? `https:${coverSrc}` : coverSrc)
72
- : undefined;
73
-
74
- // 電子版価格・DRM
75
- const ebookGuide = doc.selectOne(".module-e-book-buy-guide-txt");
76
- const ebookBuyBtn = doc.selectOne(".module-e-book-buy-guide-btn a");
77
-
78
- let price: number | undefined;
79
- let drm: DrmType = "social";
80
-
81
- if (ebookGuide) {
82
- const priceText = ebookGuide.find(".module-e-book-price")[0]?.text();
83
- if (priceText) price = parseJapanesePrice(priceText);
84
- drm = parseDrmType(ebookGuide.text());
85
- }
86
-
87
- const ebookStores = ebookBuyBtn
88
- ? [{ name: "インプレスブックス", url, drm }]
89
- : [];
90
-
91
- return {
92
- title,
93
- authors,
94
- publisher: "インプレスブックス",
95
- url,
96
- isbn,
97
- price,
98
- publishedAt,
99
- coverImageUrl,
100
- ebookStores,
101
- };
102
- },
103
- };
@@ -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
- };