@zonuexe/techbook-mcp 0.1.0 → 0.2.0
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.
- package/.claude/settings.local.json +13 -1
- package/.codex/skills/techbook-mcp-release-prep/SKILL.md +105 -0
- package/.github/workflows/test.yml +36 -0
- package/.oxlintrc.json +12 -0
- package/AGENTS.md +29 -1
- package/CHANGELOG.md +27 -0
- package/deno.json +3 -0
- package/dist/adapters/html/cheerio-parser.d.ts.map +1 -1
- package/dist/adapters/html/cheerio-parser.js.map +1 -1
- package/dist/adapters/publishers/base.d.ts +22 -1
- package/dist/adapters/publishers/base.d.ts.map +1 -1
- package/dist/adapters/publishers/base.js +142 -2
- package/dist/adapters/publishers/base.js.map +1 -1
- package/dist/adapters/publishers/book-tech.d.ts +3 -0
- package/dist/adapters/publishers/book-tech.d.ts.map +1 -0
- package/dist/adapters/publishers/book-tech.js +95 -0
- package/dist/adapters/publishers/book-tech.js.map +1 -0
- package/dist/adapters/publishers/born-digital.d.ts +3 -0
- package/dist/adapters/publishers/born-digital.d.ts.map +1 -0
- package/dist/adapters/publishers/born-digital.js +122 -0
- package/dist/adapters/publishers/born-digital.js.map +1 -0
- package/dist/adapters/publishers/coronasha.d.ts +3 -0
- package/dist/adapters/publishers/coronasha.d.ts.map +1 -0
- package/dist/adapters/publishers/coronasha.js +119 -0
- package/dist/adapters/publishers/coronasha.js.map +1 -0
- package/dist/adapters/publishers/impress.d.ts +3 -0
- package/dist/adapters/publishers/impress.d.ts.map +1 -0
- package/dist/adapters/publishers/impress.js +92 -0
- package/dist/adapters/publishers/impress.js.map +1 -0
- package/dist/adapters/publishers/manatee.d.ts +3 -0
- package/dist/adapters/publishers/manatee.d.ts.map +1 -0
- package/dist/adapters/publishers/manatee.js +93 -0
- package/dist/adapters/publishers/manatee.js.map +1 -0
- package/dist/adapters/publishers/maruzen-publishing.d.ts +3 -0
- package/dist/adapters/publishers/maruzen-publishing.d.ts.map +1 -0
- package/dist/adapters/publishers/maruzen-publishing.js +108 -0
- package/dist/adapters/publishers/maruzen-publishing.js.map +1 -0
- package/dist/adapters/publishers/optronics.d.ts +3 -0
- package/dist/adapters/publishers/optronics.d.ts.map +1 -0
- package/dist/adapters/publishers/optronics.js +92 -0
- package/dist/adapters/publishers/optronics.js.map +1 -0
- package/dist/adapters/publishers/oreilly-japan.d.ts +3 -0
- package/dist/adapters/publishers/oreilly-japan.d.ts.map +1 -0
- package/dist/adapters/publishers/oreilly-japan.js +112 -0
- package/dist/adapters/publishers/oreilly-japan.js.map +1 -0
- package/dist/adapters/publishers/peaks.d.ts +3 -0
- package/dist/adapters/publishers/peaks.d.ts.map +1 -0
- package/dist/adapters/publishers/peaks.js +80 -0
- package/dist/adapters/publishers/peaks.js.map +1 -0
- package/dist/adapters/publishers/personal-media.d.ts +3 -0
- package/dist/adapters/publishers/personal-media.d.ts.map +1 -0
- package/dist/adapters/publishers/personal-media.js +144 -0
- package/dist/adapters/publishers/personal-media.js.map +1 -0
- package/dist/adapters/publishers/registry.d.ts.map +1 -1
- package/dist/adapters/publishers/registry.js +26 -0
- package/dist/adapters/publishers/registry.js.map +1 -1
- package/dist/adapters/publishers/rutles.d.ts +3 -0
- package/dist/adapters/publishers/rutles.d.ts.map +1 -0
- package/dist/adapters/publishers/rutles.js +128 -0
- package/dist/adapters/publishers/rutles.js.map +1 -0
- package/dist/adapters/publishers/saiensu.d.ts +3 -0
- package/dist/adapters/publishers/saiensu.d.ts.map +1 -0
- package/dist/adapters/publishers/saiensu.js +109 -0
- package/dist/adapters/publishers/saiensu.js.map +1 -0
- package/dist/adapters/publishers/seshop.d.ts +3 -0
- package/dist/adapters/publishers/seshop.d.ts.map +1 -0
- package/dist/adapters/publishers/seshop.js +98 -0
- package/dist/adapters/publishers/seshop.js.map +1 -0
- package/dist/application/get-book-detail.d.ts.map +1 -1
- package/dist/application/get-book-detail.js +5 -0
- package/dist/application/get-book-detail.js.map +1 -1
- package/dist/application/search-books.d.ts.map +1 -1
- package/dist/application/search-books.js +7 -1
- package/dist/application/search-books.js.map +1 -1
- package/dist/domain/book.d.ts +5 -4
- package/dist/domain/book.d.ts.map +1 -1
- package/dist/main.d.ts +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/server.js.map +1 -1
- package/flake.nix +1 -1
- package/package.json +7 -5
- package/src/adapters/html/cheerio-parser.ts +4 -3
- package/src/adapters/publishers/base.ts +150 -0
- package/src/adapters/publishers/born-digital.ts +2 -17
- package/src/adapters/publishers/impress.ts +103 -0
- package/src/adapters/publishers/manatee.ts +2 -1
- package/src/adapters/publishers/maruzen-publishing.ts +4 -16
- package/src/adapters/publishers/oreilly-japan.ts +5 -10
- package/src/adapters/publishers/registry.ts +2 -0
- package/src/adapters/publishers/rutles.ts +1 -13
- package/src/adapters/publishers/saiensu.ts +5 -18
- package/src/adapters/publishers/seshop.ts +1 -1
- package/src/application/get-book-detail.ts +7 -0
- package/src/application/search-books.ts +6 -1
- package/src/main.ts +1 -0
- package/tests/fixtures/impress-detail-epub.html +746 -0
- package/tests/fixtures/impress-detail-social.html +689 -0
- package/tests/unit/adapters/base.test.ts +441 -0
- package/tests/unit/adapters/publishers/book-tech.test.ts +18 -15
- package/tests/unit/adapters/publishers/born-digital.test.ts +18 -15
- package/tests/unit/adapters/publishers/coronasha.test.ts +26 -20
- package/tests/unit/adapters/publishers/gihyo.test.ts +21 -19
- package/tests/unit/adapters/publishers/impress.test.ts +129 -0
- package/tests/unit/adapters/publishers/lambdanote.test.ts +12 -11
- package/tests/unit/adapters/publishers/manatee.test.ts +14 -12
- package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +19 -17
- package/tests/unit/adapters/publishers/optronics.test.ts +19 -16
- package/tests/unit/adapters/publishers/oreilly-japan.test.ts +19 -16
- package/tests/unit/adapters/publishers/peaks.test.ts +17 -14
- package/tests/unit/adapters/publishers/personal-media.test.ts +18 -15
- package/tests/unit/adapters/publishers/rutles.test.ts +15 -12
- package/tests/unit/adapters/publishers/saiensu.test.ts +14 -12
- package/tests/unit/adapters/publishers/seshop.test.ts +16 -13
- package/tests/unit/adapters/publishers/tatsu-zine.test.ts +13 -12
- package/tests/unit/adapters/publishers/techbookfest.test.ts +12 -11
- package/tests/unit/adapters/registry.test.ts +37 -0
- package/tests/unit/application/get-book-detail.test.ts +102 -0
- package/tests/unit/application/search-books.test.ts +137 -0
- package/vitest.config.ts +0 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, it
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { coronashaAdapter } from "../../../../src/adapters/publishers/coronasha.js";
|
|
@@ -28,7 +29,7 @@ describe("coronashaAdapter", () => {
|
|
|
28
29
|
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
29
30
|
|
|
30
31
|
// フィクスチャには電子版あり1件・電子版なし1件あり、電子版のみ返す
|
|
31
|
-
|
|
32
|
+
assert.strictEqual(results.length, 1);
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
it("タイトルを tunogaki + book-title で組み立てる", async () => {
|
|
@@ -40,7 +41,7 @@ describe("coronashaAdapter", () => {
|
|
|
40
41
|
|
|
41
42
|
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
assert.strictEqual(results[0].title, "1から始める Juliaプログラミング大全");
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
it("著者一覧を返す", async () => {
|
|
@@ -52,7 +53,7 @@ describe("coronashaAdapter", () => {
|
|
|
52
53
|
|
|
53
54
|
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
assert.deepStrictEqual(results[0].authors, ["進藤 裕之", "佐藤 建太"]);
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
it("価格・ISBN・発行日を返す", async () => {
|
|
@@ -64,7 +65,7 @@ describe("coronashaAdapter", () => {
|
|
|
64
65
|
|
|
65
66
|
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
assert.partialDeepStrictEqual(results[0], {
|
|
68
69
|
price: 3630,
|
|
69
70
|
isbn: "9784339029345",
|
|
70
71
|
publishedAt: "2023-05-01",
|
|
@@ -80,7 +81,8 @@ describe("coronashaAdapter", () => {
|
|
|
80
81
|
|
|
81
82
|
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
assert.strictEqual(
|
|
85
|
+
results[0].coverImageUrl,
|
|
84
86
|
"https://www.coronasha.co.jp/np/images/isbn/9784339029345_main.jpg",
|
|
85
87
|
);
|
|
86
88
|
});
|
|
@@ -94,7 +96,7 @@ describe("coronashaAdapter", () => {
|
|
|
94
96
|
|
|
95
97
|
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
assert.strictEqual(results[0].publisher, "コロナ社");
|
|
98
100
|
});
|
|
99
101
|
|
|
100
102
|
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
@@ -102,8 +104,8 @@ describe("coronashaAdapter", () => {
|
|
|
102
104
|
|
|
103
105
|
const results = await coronashaAdapter.search({}, makeDeps(http));
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
assert.deepStrictEqual(results, []);
|
|
108
|
+
assert.strictEqual(http.calls.length, 0);
|
|
107
109
|
});
|
|
108
110
|
});
|
|
109
111
|
|
|
@@ -120,7 +122,7 @@ describe("coronashaAdapter", () => {
|
|
|
120
122
|
makeDeps(http),
|
|
121
123
|
);
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
assert.partialDeepStrictEqual(book, {
|
|
124
126
|
title: "1から始める Juliaプログラミング大全",
|
|
125
127
|
publisher: "コロナ社",
|
|
126
128
|
price: 3630,
|
|
@@ -141,7 +143,7 @@ describe("coronashaAdapter", () => {
|
|
|
141
143
|
makeDeps(http),
|
|
142
144
|
);
|
|
143
145
|
|
|
144
|
-
|
|
146
|
+
assert.deepStrictEqual(book.authors, ["進藤 裕之", "佐藤 建太"]);
|
|
145
147
|
});
|
|
146
148
|
|
|
147
149
|
it("ebookStores に Kindle・Kinoppy・VarsityWave eBooks が含まれる", async () => {
|
|
@@ -156,13 +158,16 @@ describe("coronashaAdapter", () => {
|
|
|
156
158
|
makeDeps(http),
|
|
157
159
|
);
|
|
158
160
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
for (const { name, drm } of [
|
|
162
|
+
{ name: "Kindle", drm: "drm" },
|
|
163
|
+
{ name: "Kinoppy", drm: "drm" },
|
|
164
|
+
{ name: "VarsityWave eBooks", drm: "drm" },
|
|
165
|
+
]) {
|
|
166
|
+
assert.ok(
|
|
167
|
+
book.ebookStores.some(s => s.name === name && s.drm === drm),
|
|
168
|
+
`Expected store: ${name} (${drm})`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
166
171
|
});
|
|
167
172
|
|
|
168
173
|
it("Knowledge Worker (kw.maruzen.co.jp) は ebookStores に含まれない", async () => {
|
|
@@ -178,7 +183,7 @@ describe("coronashaAdapter", () => {
|
|
|
178
183
|
);
|
|
179
184
|
|
|
180
185
|
const names = book.ebookStores.map(s => s.name);
|
|
181
|
-
|
|
186
|
+
assert.ok(!names.includes("Knowledge Worker"));
|
|
182
187
|
});
|
|
183
188
|
|
|
184
189
|
it("coverImageUrl が絶対URLになる", async () => {
|
|
@@ -193,7 +198,8 @@ describe("coronashaAdapter", () => {
|
|
|
193
198
|
makeDeps(http),
|
|
194
199
|
);
|
|
195
200
|
|
|
196
|
-
|
|
201
|
+
assert.strictEqual(
|
|
202
|
+
book.coverImageUrl,
|
|
197
203
|
"https://www.coronasha.co.jp/np/images/isbn/9784339029345_main.jpg",
|
|
198
204
|
);
|
|
199
205
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, it
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { gihyoAdapter } from "../../../../src/adapters/publishers/gihyo.js";
|
|
@@ -42,8 +43,8 @@ describe("gihyoAdapter", () => {
|
|
|
42
43
|
|
|
43
44
|
const results = await gihyoAdapter.search({ title: "TypeScript", limit: 10 }, deps);
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
assert.strictEqual(results.length, 2);
|
|
47
|
+
assert.partialDeepStrictEqual(results[0], {
|
|
47
48
|
title: "プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで",
|
|
48
49
|
authors: ["uhyo"],
|
|
49
50
|
publisher: "技術評論社",
|
|
@@ -51,8 +52,9 @@ describe("gihyoAdapter", () => {
|
|
|
51
52
|
price: 3740,
|
|
52
53
|
publishedAt: "2022-04-01",
|
|
53
54
|
});
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
assert.strictEqual(results[0].url, "https://gihyo.jp/book/2022/978-4-297-12815-2");
|
|
56
|
+
assert.strictEqual(
|
|
57
|
+
results[0].coverImageUrl,
|
|
56
58
|
"https://gihyo.jp/assets/images/cover/2022/9784297128152.jpg",
|
|
57
59
|
);
|
|
58
60
|
});
|
|
@@ -62,7 +64,7 @@ describe("gihyoAdapter", () => {
|
|
|
62
64
|
const results = await gihyoAdapter.search({ title: "TypeScript" }, deps);
|
|
63
65
|
|
|
64
66
|
const book = results.find(b => b.isbn === "9784297136010");
|
|
65
|
-
|
|
67
|
+
assert.strictEqual(book?.title, "TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発");
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
it("複数著者が配列になる", async () => {
|
|
@@ -70,7 +72,7 @@ describe("gihyoAdapter", () => {
|
|
|
70
72
|
const results = await gihyoAdapter.search({ title: "TypeScript" }, deps);
|
|
71
73
|
|
|
72
74
|
const book = results.find(b => b.isbn === "9784297136010");
|
|
73
|
-
|
|
75
|
+
assert.deepStrictEqual(book?.authors, ["手島拓也", "吉田健人", "高林佳稀"]);
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
@@ -79,22 +81,22 @@ describe("gihyoAdapter", () => {
|
|
|
79
81
|
|
|
80
82
|
const results = await gihyoAdapter.search({}, deps);
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
assert.deepStrictEqual(results, []);
|
|
85
|
+
assert.strictEqual(http.calls.length, 0);
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
it("search() のリクエストURLにクエリが含まれる", async () => {
|
|
87
89
|
const deps = await makeSearchDeps("gihyo-search.json");
|
|
88
90
|
await gihyoAdapter.search({ title: "TypeScript" }, deps);
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
assert.ok(deps.http.calls[0].includes("search=TypeScript"));
|
|
91
93
|
});
|
|
92
94
|
|
|
93
95
|
it("author クエリが API に渡される", async () => {
|
|
94
96
|
const deps = await makeSearchDeps("gihyo-search.json");
|
|
95
97
|
await gihyoAdapter.search({ author: "uhyo" }, deps);
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
assert.ok(deps.http.calls[0].includes("search=uhyo"));
|
|
98
100
|
});
|
|
99
101
|
|
|
100
102
|
describe("getDetail()", () => {
|
|
@@ -105,21 +107,21 @@ describe("gihyoAdapter", () => {
|
|
|
105
107
|
deps,
|
|
106
108
|
);
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
assert.ok(book.ebookStores !== undefined);
|
|
109
111
|
const socialStores = book.ebookStores!.filter(s => s.drm === "social");
|
|
110
112
|
const drmStores = book.ebookStores!.filter(s => s.drm === "drm");
|
|
111
113
|
|
|
112
114
|
// Gihyo Digital Publishing は見えない購入者情報を埋め込むソーシャルDRM
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
assert.strictEqual(socialStores.length, 1);
|
|
116
|
+
assert.partialDeepStrictEqual(socialStores[0], {
|
|
115
117
|
name: "Gihyo Digital Publishing",
|
|
116
118
|
drm: "social",
|
|
117
|
-
url: expect.stringContaining("gihyo.jp/dp/ebook/"),
|
|
118
119
|
});
|
|
120
|
+
assert.ok(socialStores[0].url.includes("gihyo.jp/dp/ebook/"));
|
|
119
121
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
for (const name of ["Kindle", "楽天Kobo", "BookLive", "honto"]) {
|
|
123
|
+
assert.ok(drmStores.some(s => s.name === name), `Expected DRM store: ${name}`);
|
|
124
|
+
}
|
|
123
125
|
});
|
|
124
126
|
|
|
125
127
|
it("ASIN が Amazon リンクから抽出される", async () => {
|
|
@@ -129,7 +131,7 @@ describe("gihyoAdapter", () => {
|
|
|
129
131
|
deps,
|
|
130
132
|
);
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
assert.strictEqual(book.asin, "B09YGZ18ZK");
|
|
133
135
|
});
|
|
134
136
|
});
|
|
135
137
|
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { impressBooksAdapter } from "../../../../src/adapters/publishers/impress.js";
|
|
6
|
+
import { MockHttpClient } from "../../../../src/adapters/http/mock-client.js";
|
|
7
|
+
import { CheerioHtmlParser } from "../../../../src/adapters/html/cheerio-parser.js";
|
|
8
|
+
import { NullCacheStore } from "../../../../src/adapters/cache/null-cache.js";
|
|
9
|
+
|
|
10
|
+
const FIXTURES_DIR = join(import.meta.dirname, "../../../fixtures");
|
|
11
|
+
const DETAIL_SOCIAL_URL = "https://book.impress.co.jp/books/1125101113";
|
|
12
|
+
const DETAIL_EPUB_URL = "https://book.impress.co.jp/books/1124101031";
|
|
13
|
+
|
|
14
|
+
function makeDeps(http: MockHttpClient) {
|
|
15
|
+
return { http, parser: new CheerioHtmlParser(), cache: new NullCacheStore() };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function loadFixture(name: string): Promise<string> {
|
|
19
|
+
return readFile(join(FIXTURES_DIR, name), "utf-8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("impressBooksAdapter", () => {
|
|
23
|
+
describe("search()", () => {
|
|
24
|
+
it("検索APIがないため常に [] を返しHTTPを呼ばない", async () => {
|
|
25
|
+
const http = new MockHttpClient();
|
|
26
|
+
const results = await impressBooksAdapter.search({ title: "Python" }, makeDeps(http));
|
|
27
|
+
assert.deepStrictEqual(results, []);
|
|
28
|
+
assert.strictEqual(http.calls.length, 0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("クエリが空でも [] を返しHTTPを呼ばない", async () => {
|
|
32
|
+
const http = new MockHttpClient();
|
|
33
|
+
const results = await impressBooksAdapter.search({}, makeDeps(http));
|
|
34
|
+
assert.deepStrictEqual(results, []);
|
|
35
|
+
assert.strictEqual(http.calls.length, 0);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("getDetail() - ソーシャルDRM書籍", () => {
|
|
40
|
+
it("タイトルを返す", async () => {
|
|
41
|
+
const body = await loadFixture("impress-detail-social.html");
|
|
42
|
+
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
43
|
+
|
|
44
|
+
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
45
|
+
|
|
46
|
+
assert.strictEqual(
|
|
47
|
+
book.title,
|
|
48
|
+
"いちばんやさしい 先生が校務に使えるGoogle NotebookLMの教本 人気講師が教える学校業務を効率化するAI活用法",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("著者名から役割語を除いて返す", async () => {
|
|
53
|
+
const body = await loadFixture("impress-detail-social.html");
|
|
54
|
+
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
55
|
+
|
|
56
|
+
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
57
|
+
|
|
58
|
+
assert.deepStrictEqual(book.authors, ["山本康太"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("ISBN・発売日・価格を返す", async () => {
|
|
62
|
+
const body = await loadFixture("impress-detail-social.html");
|
|
63
|
+
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
64
|
+
|
|
65
|
+
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
66
|
+
|
|
67
|
+
assert.partialDeepStrictEqual(book, {
|
|
68
|
+
isbn: "9784295023654",
|
|
69
|
+
publishedAt: "2026-01-22",
|
|
70
|
+
price: 1980,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("publisher が インプレスブックス になる", async () => {
|
|
75
|
+
const body = await loadFixture("impress-detail-social.html");
|
|
76
|
+
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
77
|
+
|
|
78
|
+
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
79
|
+
|
|
80
|
+
assert.strictEqual(book.publisher, "インプレスブックス");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("ebookStores に インプレスブックス (social) が含まれる", async () => {
|
|
84
|
+
const body = await loadFixture("impress-detail-social.html");
|
|
85
|
+
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
86
|
+
|
|
87
|
+
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
88
|
+
|
|
89
|
+
assert.deepStrictEqual(book.ebookStores, [
|
|
90
|
+
{ name: "インプレスブックス", url: DETAIL_SOCIAL_URL, drm: "social" },
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("coverImageUrl が https: から始まる絶対URL になる", async () => {
|
|
95
|
+
const body = await loadFixture("impress-detail-social.html");
|
|
96
|
+
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
97
|
+
|
|
98
|
+
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
99
|
+
|
|
100
|
+
assert.strictEqual(
|
|
101
|
+
book.coverImageUrl,
|
|
102
|
+
"https://img.ips.co.jp/ij/25/1125101113/1125101113-520x.jpg",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("getDetail() - EPUB書籍", () => {
|
|
108
|
+
it("著者名から役割語を除いて返す", async () => {
|
|
109
|
+
const body = await loadFixture("impress-detail-epub.html");
|
|
110
|
+
const http = new MockHttpClient().addResponse(DETAIL_EPUB_URL, { status: 200, body });
|
|
111
|
+
|
|
112
|
+
const book = await impressBooksAdapter.getDetail(DETAIL_EPUB_URL, makeDeps(http));
|
|
113
|
+
|
|
114
|
+
assert.deepStrictEqual(book.authors, ["廣瀬 豪"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("DRM情報が明示されない EPUB 書籍は social として返す", async () => {
|
|
118
|
+
const body = await loadFixture("impress-detail-epub.html");
|
|
119
|
+
const http = new MockHttpClient().addResponse(DETAIL_EPUB_URL, { status: 200, body });
|
|
120
|
+
|
|
121
|
+
const book = await impressBooksAdapter.getDetail(DETAIL_EPUB_URL, makeDeps(http));
|
|
122
|
+
|
|
123
|
+
assert.partialDeepStrictEqual(book.ebookStores[0], {
|
|
124
|
+
name: "インプレスブックス",
|
|
125
|
+
drm: "social",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, it
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { lambdanoteAdapter } from "../../../../src/adapters/publishers/lambdanote.js";
|
|
@@ -22,14 +23,14 @@ describe("lambdanoteAdapter", () => {
|
|
|
22
23
|
|
|
23
24
|
const results = await lambdanoteAdapter.search({ title: "Go" }, makeDeps(http));
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
assert.strictEqual(results.length, 2);
|
|
27
|
+
assert.partialDeepStrictEqual(results[0], {
|
|
27
28
|
title: "Goならわかるシステムプログラミング 第2版",
|
|
28
29
|
publisher: "ラムダノート",
|
|
29
30
|
price: 3960,
|
|
30
31
|
});
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
assert.strictEqual(results[0].url, "https://www.lambdanote.com/products/go-2");
|
|
33
|
+
assert.ok(results[0].coverImageUrl?.includes("go2_small.jpg"));
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
it("2件目の書籍も正しく取得できる", async () => {
|
|
@@ -41,11 +42,11 @@ describe("lambdanoteAdapter", () => {
|
|
|
41
42
|
|
|
42
43
|
const results = await lambdanoteAdapter.search({ title: "Go" }, makeDeps(http));
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
assert.partialDeepStrictEqual(results[1], {
|
|
45
46
|
title: "プログラミング言語Go",
|
|
46
47
|
price: 4400,
|
|
47
48
|
});
|
|
48
|
-
|
|
49
|
+
assert.strictEqual(results[1].url, "https://www.lambdanote.com/products/gopl");
|
|
49
50
|
});
|
|
50
51
|
|
|
51
52
|
it("limit を適用する", async () => {
|
|
@@ -57,7 +58,7 @@ describe("lambdanoteAdapter", () => {
|
|
|
57
58
|
|
|
58
59
|
const results = await lambdanoteAdapter.search({ title: "Go", limit: 1 }, makeDeps(http));
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
assert.strictEqual(results.length, 1);
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
it("結果ゼロの場合は [] を返す", async () => {
|
|
@@ -71,14 +72,14 @@ describe("lambdanoteAdapter", () => {
|
|
|
71
72
|
|
|
72
73
|
const results = await lambdanoteAdapter.search({ title: "存在しない本" }, makeDeps(http));
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
assert.deepStrictEqual(results, []);
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
78
79
|
const http = new MockHttpClient();
|
|
79
80
|
const results = await lambdanoteAdapter.search({}, makeDeps(http));
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
assert.deepStrictEqual(results, []);
|
|
83
|
+
assert.strictEqual(http.calls.length, 0);
|
|
83
84
|
});
|
|
84
85
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, it
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { manateeAdapter } from "../../../../src/adapters/publishers/manatee.js";
|
|
@@ -27,8 +28,8 @@ describe("manateeAdapter", () => {
|
|
|
27
28
|
|
|
28
29
|
const results = await manateeAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
assert.strictEqual(results.length, 3);
|
|
32
|
+
assert.partialDeepStrictEqual(results[0], {
|
|
32
33
|
title: "現場で使えるTypeScript 詳解実践ガイド",
|
|
33
34
|
publisher: "マナティ",
|
|
34
35
|
url: "https://book.mynavi.jp/manatee/books/detail/id=142711",
|
|
@@ -45,7 +46,7 @@ describe("manateeAdapter", () => {
|
|
|
45
46
|
|
|
46
47
|
const results = await manateeAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
assert.deepStrictEqual(results[0].ebookStores, [
|
|
49
50
|
{
|
|
50
51
|
name: "マナティ",
|
|
51
52
|
url: "https://book.mynavi.jp/manatee/books/detail/id=142711",
|
|
@@ -63,7 +64,8 @@ describe("manateeAdapter", () => {
|
|
|
63
64
|
|
|
64
65
|
const results = await manateeAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
assert.strictEqual(
|
|
68
|
+
results[0].coverImageUrl,
|
|
67
69
|
"https://book.mynavi.jp/files/topics/142711_ext_06_0.jpg",
|
|
68
70
|
);
|
|
69
71
|
});
|
|
@@ -77,7 +79,7 @@ describe("manateeAdapter", () => {
|
|
|
77
79
|
|
|
78
80
|
const results = await manateeAdapter.search({ title: "TypeScript", limit: 2 }, makeDeps(http));
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
assert.strictEqual(results.length, 2);
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
@@ -85,8 +87,8 @@ describe("manateeAdapter", () => {
|
|
|
85
87
|
|
|
86
88
|
const results = await manateeAdapter.search({}, makeDeps(http));
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
assert.deepStrictEqual(results, []);
|
|
91
|
+
assert.strictEqual(http.calls.length, 0);
|
|
90
92
|
});
|
|
91
93
|
|
|
92
94
|
it("検索リクエストに topics_keyword が含まれる", async () => {
|
|
@@ -98,7 +100,7 @@ describe("manateeAdapter", () => {
|
|
|
98
100
|
|
|
99
101
|
await manateeAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
assert.ok(http.calls[0].includes("topics_keyword=TypeScript"));
|
|
102
104
|
});
|
|
103
105
|
});
|
|
104
106
|
|
|
@@ -115,7 +117,7 @@ describe("manateeAdapter", () => {
|
|
|
115
117
|
makeDeps(http),
|
|
116
118
|
);
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
assert.partialDeepStrictEqual(book, {
|
|
119
121
|
title: "現場で使えるTypeScript 詳解実践ガイド",
|
|
120
122
|
publisher: "マイナビ出版",
|
|
121
123
|
isbn: "9784839984274",
|
|
@@ -136,7 +138,7 @@ describe("manateeAdapter", () => {
|
|
|
136
138
|
makeDeps(http),
|
|
137
139
|
);
|
|
138
140
|
|
|
139
|
-
|
|
141
|
+
assert.deepStrictEqual(book.authors, ["菅原浩之", "CodeMafia", "外村将大"]);
|
|
140
142
|
});
|
|
141
143
|
|
|
142
144
|
it("ebookStores にマナティ(ソーシャルDRM)が含まれる", async () => {
|
|
@@ -151,7 +153,7 @@ describe("manateeAdapter", () => {
|
|
|
151
153
|
makeDeps(http),
|
|
152
154
|
);
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
assert.deepStrictEqual(book.ebookStores, [
|
|
155
157
|
{
|
|
156
158
|
name: "マナティ",
|
|
157
159
|
url: "https://book.mynavi.jp/manatee/books/detail/id=142711",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe, it
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { maruzenPublishingAdapter } from "../../../../src/adapters/publishers/maruzen-publishing.js";
|
|
@@ -27,8 +28,8 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
27
28
|
|
|
28
29
|
const results = await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
assert.ok(results.length >= 1);
|
|
32
|
+
assert.partialDeepStrictEqual(results[0], {
|
|
32
33
|
title: "プログラミングTypeScript",
|
|
33
34
|
publisher: "丸善出版",
|
|
34
35
|
url: "https://www.maruzen-publishing.co.jp/book/b10152370.html",
|
|
@@ -44,7 +45,7 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
44
45
|
|
|
45
46
|
const results = await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
assert.deepStrictEqual(results[0].authors, ["ボリス・チェルニー", "折山文哉"]);
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
it("coverImageUrl が設定される", async () => {
|
|
@@ -56,7 +57,8 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
56
57
|
|
|
57
58
|
const results = await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
assert.strictEqual(
|
|
61
|
+
results[0].coverImageUrl,
|
|
60
62
|
"https://www.maruzen-publishing.co.jp/files/isbn/978-4-621-30855-1.jpg",
|
|
61
63
|
);
|
|
62
64
|
});
|
|
@@ -66,8 +68,8 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
66
68
|
|
|
67
69
|
const results = await maruzenPublishingAdapter.search({}, makeDeps(http));
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
assert.deepStrictEqual(results, []);
|
|
72
|
+
assert.strictEqual(http.calls.length, 0);
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
it("検索リクエストに search_keyword が含まれる", async () => {
|
|
@@ -79,7 +81,7 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
79
81
|
|
|
80
82
|
await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
assert.ok(http.calls[0].includes("search_keyword=TypeScript"));
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
it("検索リクエストに format=1 が含まれる", async () => {
|
|
@@ -91,7 +93,7 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
91
93
|
|
|
92
94
|
await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
assert.ok(http.calls[0].includes("format=1"));
|
|
95
97
|
});
|
|
96
98
|
|
|
97
99
|
it("3件取得できる", async () => {
|
|
@@ -103,7 +105,7 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
103
105
|
|
|
104
106
|
const results = await maruzenPublishingAdapter.search({ title: "プログラム" }, makeDeps(http));
|
|
105
107
|
|
|
106
|
-
|
|
108
|
+
assert.strictEqual(results.length, 3);
|
|
107
109
|
});
|
|
108
110
|
|
|
109
111
|
it("監訳者の役割語も除去する", async () => {
|
|
@@ -116,7 +118,7 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
116
118
|
const results = await maruzenPublishingAdapter.search({ title: "統計" }, makeDeps(http));
|
|
117
119
|
const statsBook = results.find(r => r.title.includes("統計"));
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
assert.deepStrictEqual(statsBook?.authors, ["ピーター・ブルース", "アンドリュー・ブルース", "大橋真也"]);
|
|
120
122
|
});
|
|
121
123
|
});
|
|
122
124
|
|
|
@@ -133,7 +135,7 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
133
135
|
makeDeps(http),
|
|
134
136
|
);
|
|
135
137
|
|
|
136
|
-
|
|
138
|
+
assert.partialDeepStrictEqual(book, {
|
|
137
139
|
title: "プログラミングTypeScript",
|
|
138
140
|
publisher: "丸善出版",
|
|
139
141
|
publishedAt: "2020-03-31",
|
|
@@ -152,7 +154,7 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
152
154
|
makeDeps(http),
|
|
153
155
|
);
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
assert.deepStrictEqual(book.authors, ["ボリス・チェルニー", "折山文哉"]);
|
|
156
158
|
});
|
|
157
159
|
|
|
158
160
|
it("ebookStores に Kindle と Kinoppy と honto が含まれ Knowledge Worker は除外される", async () => {
|
|
@@ -168,10 +170,10 @@ describe("maruzenPublishingAdapter", () => {
|
|
|
168
170
|
);
|
|
169
171
|
|
|
170
172
|
const storeNames = book.ebookStores?.map(s => s.name) ?? [];
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
assert.ok(storeNames.includes("Kindle"));
|
|
174
|
+
assert.ok(storeNames.includes("Kinoppy"));
|
|
175
|
+
assert.ok(storeNames.includes("honto"));
|
|
176
|
+
assert.ok(!storeNames.includes("Knowledge Worker"));
|
|
175
177
|
});
|
|
176
178
|
});
|
|
177
179
|
});
|