@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.
- package/CHANGELOG.md +52 -1
- package/README.md +54 -22
- package/dist/adapters/http/fetch-client.d.ts.map +1 -1
- package/dist/adapters/http/fetch-client.js +18 -1
- package/dist/adapters/http/fetch-client.js.map +1 -1
- package/dist/adapters/openbd.d.ts.map +1 -1
- package/dist/adapters/openbd.js +18 -5
- package/dist/adapters/openbd.js.map +1 -1
- package/dist/adapters/publishers/base.d.ts +2 -1
- package/dist/adapters/publishers/base.d.ts.map +1 -1
- package/dist/adapters/publishers/base.js +8 -3
- package/dist/adapters/publishers/base.js.map +1 -1
- package/dist/adapters/publishers/cq-publishing.d.ts +3 -0
- package/dist/adapters/publishers/cq-publishing.d.ts.map +1 -0
- package/dist/adapters/publishers/cq-publishing.js +120 -0
- package/dist/adapters/publishers/cq-publishing.js.map +1 -0
- package/dist/adapters/publishers/google-books.d.ts +4 -0
- package/dist/adapters/publishers/google-books.d.ts.map +1 -0
- package/dist/adapters/publishers/google-books.js +76 -0
- package/dist/adapters/publishers/google-books.js.map +1 -0
- package/dist/adapters/publishers/isbn-publisher-codes.d.ts +21 -0
- package/dist/adapters/publishers/isbn-publisher-codes.d.ts.map +1 -0
- package/dist/adapters/publishers/isbn-publisher-codes.js +49 -0
- package/dist/adapters/publishers/isbn-publisher-codes.js.map +1 -0
- package/dist/adapters/publishers/juse-p.d.ts +3 -0
- package/dist/adapters/publishers/juse-p.d.ts.map +1 -0
- package/dist/adapters/publishers/juse-p.js +110 -0
- package/dist/adapters/publishers/juse-p.js.map +1 -0
- package/dist/adapters/publishers/leanpub.d.ts +3 -0
- package/dist/adapters/publishers/leanpub.d.ts.map +1 -0
- package/dist/adapters/publishers/leanpub.js +96 -0
- package/dist/adapters/publishers/leanpub.js.map +1 -0
- package/dist/adapters/publishers/oreilly-japan.d.ts.map +1 -1
- package/dist/adapters/publishers/oreilly-japan.js +8 -2
- package/dist/adapters/publishers/oreilly-japan.js.map +1 -1
- package/dist/adapters/publishers/peaks.d.ts.map +1 -1
- package/dist/adapters/publishers/peaks.js +3 -2
- package/dist/adapters/publishers/peaks.js.map +1 -1
- package/dist/adapters/publishers/personal-media.d.ts.map +1 -1
- package/dist/adapters/publishers/personal-media.js +3 -2
- package/dist/adapters/publishers/personal-media.js.map +1 -1
- package/dist/adapters/publishers/pragprog.d.ts +3 -0
- package/dist/adapters/publishers/pragprog.d.ts.map +1 -0
- package/dist/adapters/publishers/pragprog.js +120 -0
- package/dist/adapters/publishers/pragprog.js.map +1 -0
- package/dist/adapters/publishers/registry.d.ts.map +1 -1
- package/dist/adapters/publishers/registry.js +10 -0
- package/dist/adapters/publishers/registry.js.map +1 -1
- package/dist/adapters/publishers/techbookfest.d.ts.map +1 -1
- package/dist/adapters/publishers/techbookfest.js +2 -1
- package/dist/adapters/publishers/techbookfest.js.map +1 -1
- package/dist/application/concurrency.d.ts +16 -0
- package/dist/application/concurrency.d.ts.map +1 -0
- package/dist/application/concurrency.js +42 -0
- package/dist/application/concurrency.js.map +1 -0
- package/dist/application/get-book-by-isbn.d.ts +0 -8
- package/dist/application/get-book-by-isbn.d.ts.map +1 -1
- package/dist/application/get-book-by-isbn.js +64 -7
- package/dist/application/get-book-by-isbn.js.map +1 -1
- package/dist/application/get-book-detail.d.ts.map +1 -1
- package/dist/application/get-book-detail.js +3 -0
- package/dist/application/get-book-detail.js.map +1 -1
- package/dist/application/search-books.d.ts +16 -5
- package/dist/application/search-books.d.ts.map +1 -1
- package/dist/application/search-books.js +46 -9
- package/dist/application/search-books.js.map +1 -1
- package/dist/config/credentials.d.ts +8 -0
- package/dist/config/credentials.d.ts.map +1 -0
- package/dist/config/credentials.js +32 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/domain/authors.d.ts +7 -0
- package/dist/domain/authors.d.ts.map +1 -0
- package/dist/domain/authors.js +22 -0
- package/dist/domain/authors.js.map +1 -0
- package/dist/domain/book.d.ts +2 -0
- package/dist/domain/book.d.ts.map +1 -1
- package/dist/domain/isbn.d.ts +8 -0
- package/dist/domain/isbn.d.ts.map +1 -0
- package/dist/domain/isbn.js +16 -0
- package/dist/domain/isbn.js.map +1 -0
- package/dist/domain/publisher.d.ts +16 -0
- package/dist/domain/publisher.d.ts.map +1 -1
- package/dist/domain/text-match.d.ts +32 -0
- package/dist/domain/text-match.d.ts.map +1 -0
- package/dist/domain/text-match.js +84 -0
- package/dist/domain/text-match.js.map +1 -0
- package/dist/main.js +15 -1
- package/dist/main.js.map +1 -1
- package/dist/mcp/server.d.ts +6 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +40 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +9 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +43 -0
- package/dist/setup.js.map +1 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/docs/design-doc.md +127 -7
- package/package.json +14 -15
- package/.claude/settings.local.json +0 -38
- package/.codex/skills/techbook-mcp-release-prep/SKILL.md +0 -105
- package/.github/workflows/test.yml +0 -72
- package/.oxlintrc.json +0 -12
- package/AGENTS.md +0 -100
- package/deno.json +0 -3
- package/src/adapters/cache/memory-cache.ts +0 -31
- package/src/adapters/cache/null-cache.ts +0 -8
- package/src/adapters/calil.ts +0 -57
- package/src/adapters/html/cheerio-parser.ts +0 -50
- package/src/adapters/http/fetch-client.ts +0 -47
- package/src/adapters/http/mock-client.ts +0 -77
- package/src/adapters/openbd.ts +0 -142
- package/src/adapters/publishers/base.ts +0 -279
- package/src/adapters/publishers/book-tech.ts +0 -117
- package/src/adapters/publishers/born-digital.ts +0 -143
- package/src/adapters/publishers/coronasha.ts +0 -139
- package/src/adapters/publishers/gihyo.ts +0 -120
- package/src/adapters/publishers/impress.ts +0 -103
- package/src/adapters/publishers/lambdanote.ts +0 -146
- package/src/adapters/publishers/manatee.ts +0 -113
- package/src/adapters/publishers/maruzen-publishing.ts +0 -129
- package/src/adapters/publishers/optronics.ts +0 -113
- package/src/adapters/publishers/oreilly-japan.ts +0 -133
- package/src/adapters/publishers/peaks.ts +0 -98
- package/src/adapters/publishers/personal-media.ts +0 -168
- package/src/adapters/publishers/registry.ts +0 -38
- package/src/adapters/publishers/rutles.ts +0 -149
- package/src/adapters/publishers/saiensu.ts +0 -136
- package/src/adapters/publishers/seshop.ts +0 -121
- package/src/adapters/publishers/tatsu-zine.ts +0 -142
- package/src/adapters/publishers/techbookfest.ts +0 -179
- package/src/application/get-book-by-isbn.ts +0 -50
- package/src/application/get-book-detail.ts +0 -40
- package/src/application/search-books.ts +0 -64
- package/src/domain/book.ts +0 -35
- package/src/domain/publisher.ts +0 -18
- package/src/main.ts +0 -14
- package/src/mcp/server.ts +0 -113
- package/src/mcp/tools.ts +0 -71
- package/src/ports/cache.ts +0 -5
- package/src/ports/html-parser.ts +0 -15
- package/src/ports/http.ts +0 -17
- package/tests/fixtures/book-tech-detail.html +0 -51
- package/tests/fixtures/book-tech-search.html +0 -91
- package/tests/fixtures/born-digital-detail.html +0 -62
- package/tests/fixtures/born-digital-search.html +0 -51
- package/tests/fixtures/calil-book.html +0 -987
- package/tests/fixtures/coronasha-detail.html +0 -41
- package/tests/fixtures/coronasha-search.html +0 -61
- package/tests/fixtures/gihyo-detail.html +0 -42
- package/tests/fixtures/gihyo-search.json +0 -54
- package/tests/fixtures/impress-detail-epub.html +0 -746
- package/tests/fixtures/impress-detail-social.html +0 -689
- package/tests/fixtures/lambdanote-search.html +0 -66
- package/tests/fixtures/manatee-detail.html +0 -53
- package/tests/fixtures/manatee-search.html +0 -59
- package/tests/fixtures/maruzen-detail.html +0 -51
- package/tests/fixtures/maruzen-search.html +0 -60
- package/tests/fixtures/openbd-response.json +0 -110
- package/tests/fixtures/optronics-detail.html +0 -30
- package/tests/fixtures/optronics-search.html +0 -75
- package/tests/fixtures/oreilly-detail.html +0 -52
- package/tests/fixtures/oreilly-ebook-list.html +0 -53
- package/tests/fixtures/peaks-detail.html +0 -39
- package/tests/fixtures/peaks-top.html +0 -50
- package/tests/fixtures/personal-media-detail.html +0 -32
- package/tests/fixtures/personal-media-search.html +0 -39
- package/tests/fixtures/rutles-detail.html +0 -32
- package/tests/fixtures/rutles-search.html +0 -62
- package/tests/fixtures/saiensu-detail.html +0 -41
- package/tests/fixtures/saiensu-search.html +0 -65
- package/tests/fixtures/seshop-detail.html +0 -45
- package/tests/fixtures/seshop-search.html +0 -58
- package/tests/fixtures/tatsu-zine-detail-free.html +0 -24
- package/tests/fixtures/tatsu-zine-search.html +0 -40
- package/tests/fixtures/techbookfest-search.json +0 -73
- package/tests/unit/adapters/base.test.ts +0 -441
- package/tests/unit/adapters/calil.test.ts +0 -69
- package/tests/unit/adapters/openbd.test.ts +0 -185
- package/tests/unit/adapters/publishers/book-tech.test.ts +0 -186
- package/tests/unit/adapters/publishers/born-digital.test.ts +0 -194
- package/tests/unit/adapters/publishers/coronasha.test.ts +0 -207
- package/tests/unit/adapters/publishers/gihyo.test.ts +0 -137
- package/tests/unit/adapters/publishers/impress.test.ts +0 -129
- package/tests/unit/adapters/publishers/lambdanote.test.ts +0 -85
- package/tests/unit/adapters/publishers/manatee.test.ts +0 -165
- package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +0 -179
- package/tests/unit/adapters/publishers/optronics.test.ts +0 -208
- package/tests/unit/adapters/publishers/oreilly-japan.test.ts +0 -194
- package/tests/unit/adapters/publishers/peaks.test.ts +0 -177
- package/tests/unit/adapters/publishers/personal-media.test.ts +0 -199
- package/tests/unit/adapters/publishers/rutles.test.ts +0 -173
- package/tests/unit/adapters/publishers/saiensu.test.ts +0 -169
- package/tests/unit/adapters/publishers/seshop.test.ts +0 -174
- package/tests/unit/adapters/publishers/tatsu-zine.test.ts +0 -172
- package/tests/unit/adapters/publishers/techbookfest.test.ts +0 -94
- package/tests/unit/adapters/registry.test.ts +0 -37
- package/tests/unit/application/get-book-by-isbn.test.ts +0 -176
- package/tests/unit/application/get-book-detail.test.ts +0 -102
- package/tests/unit/application/search-books.test.ts +0 -137
- package/tsconfig.json +0 -17
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
|
|
2
|
-
import type { BookRecord, SearchQuery } from "../../domain/book.js";
|
|
3
|
-
import { fetchText, parseJapanesePrice } from "./base.js";
|
|
4
|
-
|
|
5
|
-
const BASE_URL = "https://peaks.cc";
|
|
6
|
-
|
|
7
|
-
export const peaksAdapter: PublisherAdapter = {
|
|
8
|
-
id: "peaks",
|
|
9
|
-
name: "PEAKS",
|
|
10
|
-
baseUrl: BASE_URL,
|
|
11
|
-
|
|
12
|
-
async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
|
|
13
|
-
// 検索APIなし。タイトルでトップページの全書籍をローカルフィルタリング。
|
|
14
|
-
// 著者のみの検索は各詳細ページを全取得しないと不可能なため非対応。
|
|
15
|
-
if (!query.title) return [];
|
|
16
|
-
|
|
17
|
-
const keyword = query.title.toLowerCase();
|
|
18
|
-
const html = await fetchText(BASE_URL, deps);
|
|
19
|
-
const doc = deps.parser.parse(html);
|
|
20
|
-
const limit = query.limit ?? 10;
|
|
21
|
-
|
|
22
|
-
const results: BookRecord[] = [];
|
|
23
|
-
|
|
24
|
-
// トップページ掲載の全書籍: article.p-project__unit
|
|
25
|
-
for (const article of doc.select("article.p-project__unit")) {
|
|
26
|
-
const imgEl = article.find("div.p-project__unit_image img")[0];
|
|
27
|
-
const title = imgEl?.attr("alt")?.trim();
|
|
28
|
-
if (!title) continue;
|
|
29
|
-
|
|
30
|
-
if (!title.toLowerCase().includes(keyword)) continue;
|
|
31
|
-
|
|
32
|
-
const linkEl = article.find("div.p-project__unit_image a")[0];
|
|
33
|
-
const href = linkEl?.attr("href");
|
|
34
|
-
if (!href) continue;
|
|
35
|
-
|
|
36
|
-
const bookUrl = `${BASE_URL}${href}`;
|
|
37
|
-
const coverImageUrl = imgEl.attr("src") ?? undefined;
|
|
38
|
-
|
|
39
|
-
results.push({
|
|
40
|
-
title,
|
|
41
|
-
authors: [],
|
|
42
|
-
publisher: "PEAKS",
|
|
43
|
-
url: bookUrl,
|
|
44
|
-
coverImageUrl,
|
|
45
|
-
ebookStores: [{ name: "PEAKS", url: bookUrl, drm: "free" }],
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (results.length >= limit) break;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return results;
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
|
|
55
|
-
const html = await fetchText(url, deps);
|
|
56
|
-
const doc = deps.parser.parse(html);
|
|
57
|
-
|
|
58
|
-
const getMeta = (name: string, attr = "content"): string | undefined =>
|
|
59
|
-
doc.selectOne(`meta[name="${name}"]`)?.attr(attr)?.trim() ||
|
|
60
|
-
doc.selectOne(`meta[property="${name}"]`)?.attr("content")?.trim() ||
|
|
61
|
-
undefined;
|
|
62
|
-
|
|
63
|
-
// タイトル: twitter:title(『』なし)が最もクリーン
|
|
64
|
-
const title =
|
|
65
|
-
getMeta("twitter:title") ||
|
|
66
|
-
doc.selectOne("h2.p-project__intro_title")?.text()
|
|
67
|
-
.trim().replace(/^[『「]|[』」]$/g, "") ||
|
|
68
|
-
"";
|
|
69
|
-
|
|
70
|
-
// 著者: p-project__intro_author 内のリンクテキスト(末尾カンマを除去)
|
|
71
|
-
const authors = doc.select("ul.p-project__intro_author a")
|
|
72
|
-
.map(el => el.text().trim().replace(/,\s*$/, ""))
|
|
73
|
-
.filter(Boolean);
|
|
74
|
-
|
|
75
|
-
// 価格: 電子版 option の span テキスト
|
|
76
|
-
const priceEl = doc.selectOne("option[kind='electronic'] span") ??
|
|
77
|
-
doc.selectOne("select#editions option");
|
|
78
|
-
const price = priceEl
|
|
79
|
-
? parseJapanesePrice(priceEl.text())
|
|
80
|
-
: undefined;
|
|
81
|
-
|
|
82
|
-
// カバー画像: twitter:image または og:image
|
|
83
|
-
const coverImageUrl =
|
|
84
|
-
getMeta("twitter:image") ||
|
|
85
|
-
getMeta("og:image") ||
|
|
86
|
-
undefined;
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
title,
|
|
90
|
-
authors,
|
|
91
|
-
publisher: "PEAKS",
|
|
92
|
-
url,
|
|
93
|
-
price,
|
|
94
|
-
coverImageUrl,
|
|
95
|
-
ebookStores: [{ name: "PEAKS", url, drm: "free" }],
|
|
96
|
-
};
|
|
97
|
-
},
|
|
98
|
-
};
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
|
|
2
|
-
import type { BookRecord, EbookStore, SearchQuery } from "../../domain/book.js";
|
|
3
|
-
import { fetchText, parseJapanesePrice, resolveUrl } from "./base.js";
|
|
4
|
-
|
|
5
|
-
const BASE_URL = "https://www.personal-media.co.jp";
|
|
6
|
-
const WEBSHOP_URL = `${BASE_URL}/webshop/book/`;
|
|
7
|
-
|
|
8
|
-
/** "2001年11月 発売" → "2001-11-01" (日不明のため1日固定) */
|
|
9
|
-
function parseYearMonth(text: string): string | undefined {
|
|
10
|
-
const m = text.match(/(\d{4})年(\d{1,2})月/);
|
|
11
|
-
if (!m) return undefined;
|
|
12
|
-
return `${m[1]}-${m[2].padStart(2, "0")}-01`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* テキスト行が著者行であれば著者名配列を返す。
|
|
17
|
-
* "坂村 健 監修" → ["坂村 健"]
|
|
18
|
-
* "A, B 著" → ["A", "B"]
|
|
19
|
-
*/
|
|
20
|
-
function parseAuthorLine(text: string): string[] | null {
|
|
21
|
-
const m = text.match(/^(.+?)\s+(?:著者|著|監修|編著|編集|編|訳|監訳|共著|共編|翻訳)$/);
|
|
22
|
-
if (!m) return null;
|
|
23
|
-
return m[1].split(/[,,、・]/).map(n => n.trim()).filter(Boolean);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const personalMediaAdapter: PublisherAdapter = {
|
|
27
|
-
id: "personal-media",
|
|
28
|
-
name: "パーソナルメディア",
|
|
29
|
-
baseUrl: BASE_URL,
|
|
30
|
-
|
|
31
|
-
async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
|
|
32
|
-
// ローカルフィルタ型:タイトルなし検索は非対応
|
|
33
|
-
if (!query.title) return [];
|
|
34
|
-
|
|
35
|
-
const html = await fetchText(WEBSHOP_URL, deps);
|
|
36
|
-
const doc = deps.parser.parse(html);
|
|
37
|
-
|
|
38
|
-
const results: BookRecord[] = [];
|
|
39
|
-
const limit = query.limit ?? 10;
|
|
40
|
-
const keyword = query.title.toLowerCase();
|
|
41
|
-
|
|
42
|
-
for (const tr of doc.select("table tr")) {
|
|
43
|
-
// ヘッダー行をスキップ
|
|
44
|
-
if (tr.find("th").length > 0) continue;
|
|
45
|
-
|
|
46
|
-
// 最初の td a が書籍詳細ページへのリンク
|
|
47
|
-
const linkEl = tr.find("td a")[0];
|
|
48
|
-
if (!linkEl) continue;
|
|
49
|
-
|
|
50
|
-
const title = linkEl.text().trim();
|
|
51
|
-
if (!title) continue;
|
|
52
|
-
if (!title.toLowerCase().includes(keyword)) continue;
|
|
53
|
-
|
|
54
|
-
const href = linkEl.attr("href");
|
|
55
|
-
if (!href || !href.includes("/book/")) continue;
|
|
56
|
-
|
|
57
|
-
const bookUrl = resolveUrl(WEBSHOP_URL, href);
|
|
58
|
-
|
|
59
|
-
// 最後の td が税込価格
|
|
60
|
-
const tds = tr.find("td");
|
|
61
|
-
const price = tds.length > 0
|
|
62
|
-
? parseJapanesePrice(tds[tds.length - 1].text())
|
|
63
|
-
: undefined;
|
|
64
|
-
|
|
65
|
-
results.push({
|
|
66
|
-
title,
|
|
67
|
-
authors: [],
|
|
68
|
-
publisher: "パーソナルメディア",
|
|
69
|
-
url: bookUrl,
|
|
70
|
-
price,
|
|
71
|
-
ebookStores: [],
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
if (results.length >= limit) break;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return results;
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
|
|
81
|
-
const html = await fetchText(url, deps);
|
|
82
|
-
const doc = deps.parser.parse(html);
|
|
83
|
-
|
|
84
|
-
const title = doc.selectOne("h1")?.text().trim() ?? "";
|
|
85
|
-
|
|
86
|
-
// カバー画像("_l." パターンで書影を識別)
|
|
87
|
-
const coverImgEl = doc.select("img").find(
|
|
88
|
-
el => (el.attr("src") ?? "").includes("_l."),
|
|
89
|
-
);
|
|
90
|
-
const coverSrc = coverImgEl?.attr("src");
|
|
91
|
-
const coverImageUrl = coverSrc ? resolveUrl(url, coverSrc) : undefined;
|
|
92
|
-
|
|
93
|
-
// ページ全体テキストを行分割してメタデータを抽出
|
|
94
|
-
const lines = (doc.selectOne("body")?.text() ?? "")
|
|
95
|
-
.split(/[\n\r]+/)
|
|
96
|
-
.map(l => l.trim())
|
|
97
|
-
.filter(Boolean);
|
|
98
|
-
|
|
99
|
-
const authors: string[] = [];
|
|
100
|
-
let isbn: string | undefined;
|
|
101
|
-
let publishedAt: string | undefined;
|
|
102
|
-
let price: number | undefined;
|
|
103
|
-
|
|
104
|
-
for (const line of lines) {
|
|
105
|
-
// 著者行
|
|
106
|
-
const authorNames = parseAuthorLine(line);
|
|
107
|
-
if (authorNames) {
|
|
108
|
-
authors.push(...authorNames);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
// ISBN("ISBN 978-..." 形式)
|
|
112
|
-
if (!isbn && line.includes("ISBN")) {
|
|
113
|
-
const m = line.match(/(\d[\d-]{12,})/);
|
|
114
|
-
if (m) isbn = m[1].replace(/-/g, "");
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
// 発行日("発売" を含む行)
|
|
118
|
-
if (!publishedAt && line.includes("発売")) {
|
|
119
|
-
publishedAt = parseYearMonth(line);
|
|
120
|
-
}
|
|
121
|
-
// 定価
|
|
122
|
-
if (!price && line.startsWith("定価")) {
|
|
123
|
-
price = parseJapanesePrice(line);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 電子書籍ストアを手動検出(相対URLのため extractEbookStoresFromDoc は不使用)
|
|
128
|
-
const ebookStores: EbookStore[] = [];
|
|
129
|
-
|
|
130
|
-
const allLinks = doc.select("a[href]");
|
|
131
|
-
|
|
132
|
-
// PDF版(ウェブショップ)
|
|
133
|
-
const webshopLink = allLinks.find(
|
|
134
|
-
el => (el.attr("href") ?? "").includes("/webshop/book/"),
|
|
135
|
-
);
|
|
136
|
-
if (webshopLink) {
|
|
137
|
-
ebookStores.push({
|
|
138
|
-
name: "パーソナルメディア",
|
|
139
|
-
url: resolveUrl(url, webshopLink.attr("href")!),
|
|
140
|
-
drm: "social",
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Smooth Reader版(専用ビューアー)
|
|
145
|
-
const smoothReaderLink = allLinks.find(
|
|
146
|
-
el => (el.attr("href") ?? "").includes("/smoothreader/store/"),
|
|
147
|
-
);
|
|
148
|
-
if (smoothReaderLink) {
|
|
149
|
-
ebookStores.push({
|
|
150
|
-
name: "Smooth Reader",
|
|
151
|
-
url: resolveUrl(url, smoothReaderLink.attr("href")!),
|
|
152
|
-
drm: "drm",
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
title,
|
|
158
|
-
authors,
|
|
159
|
-
publisher: "パーソナルメディア",
|
|
160
|
-
url,
|
|
161
|
-
isbn,
|
|
162
|
-
price,
|
|
163
|
-
publishedAt,
|
|
164
|
-
coverImageUrl,
|
|
165
|
-
ebookStores,
|
|
166
|
-
};
|
|
167
|
-
},
|
|
168
|
-
};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { PublisherAdapter } from "../../domain/publisher.js";
|
|
2
|
-
import { bookTechAdapter } from "./book-tech.js";
|
|
3
|
-
import { bornDigitalAdapter } from "./born-digital.js";
|
|
4
|
-
import { coronashaAdapter } from "./coronasha.js";
|
|
5
|
-
import { gihyoAdapter } from "./gihyo.js";
|
|
6
|
-
import { impressBooksAdapter } from "./impress.js";
|
|
7
|
-
import { lambdanoteAdapter } from "./lambdanote.js";
|
|
8
|
-
import { manateeAdapter } from "./manatee.js";
|
|
9
|
-
import { maruzenPublishingAdapter } from "./maruzen-publishing.js";
|
|
10
|
-
import { optronicsAdapter } from "./optronics.js";
|
|
11
|
-
import { oreillyJapanAdapter } from "./oreilly-japan.js";
|
|
12
|
-
import { peaksAdapter } from "./peaks.js";
|
|
13
|
-
import { personalMediaAdapter } from "./personal-media.js";
|
|
14
|
-
import { rutlesAdapter } from "./rutles.js";
|
|
15
|
-
import { saiensuAdapter } from "./saiensu.js";
|
|
16
|
-
import { seshopAdapter } from "./seshop.js";
|
|
17
|
-
import { tatsuZineAdapter } from "./tatsu-zine.js";
|
|
18
|
-
import { techbookfestAdapter } from "./techbookfest.js";
|
|
19
|
-
|
|
20
|
-
export const DEFAULT_PUBLISHERS: readonly PublisherAdapter[] = [
|
|
21
|
-
bookTechAdapter,
|
|
22
|
-
bornDigitalAdapter,
|
|
23
|
-
coronashaAdapter,
|
|
24
|
-
gihyoAdapter,
|
|
25
|
-
impressBooksAdapter,
|
|
26
|
-
lambdanoteAdapter,
|
|
27
|
-
manateeAdapter,
|
|
28
|
-
maruzenPublishingAdapter,
|
|
29
|
-
optronicsAdapter,
|
|
30
|
-
oreillyJapanAdapter,
|
|
31
|
-
peaksAdapter,
|
|
32
|
-
personalMediaAdapter,
|
|
33
|
-
rutlesAdapter,
|
|
34
|
-
saiensuAdapter,
|
|
35
|
-
seshopAdapter,
|
|
36
|
-
tatsuZineAdapter,
|
|
37
|
-
techbookfestAdapter,
|
|
38
|
-
];
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
|
|
2
|
-
import type { BookRecord, SearchQuery } from "../../domain/book.js";
|
|
3
|
-
import { fetchText, parseJapanesePrice, encodeEucJp } from "./base.js";
|
|
4
|
-
|
|
5
|
-
const BASE_URL = "https://shop.rutles.net";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* "著者: 大槻有一郎:著 山田巧(DXライブラリ管理人):監修<br />"
|
|
9
|
-
* から著者名のリストを取得する。
|
|
10
|
-
* - 役割語(:著 / :訳 / :監修 など ":"以降)を除去
|
|
11
|
-
* - 所属(括弧内)を除去
|
|
12
|
-
* - 全角スペースで複数著者を分割
|
|
13
|
-
*/
|
|
14
|
-
function parseAuthorsFromBodyText(bodyText: string): string[] {
|
|
15
|
-
const m = bodyText.match(/著者[::]\s*([^\n<]+)/);
|
|
16
|
-
if (!m) return [];
|
|
17
|
-
return m[1]
|
|
18
|
-
.split(/[\u3000 ]/) // 全角スペースで分割
|
|
19
|
-
.map(part =>
|
|
20
|
-
part
|
|
21
|
-
.replace(/\(.*?\)/g, "") // (所属) を除去
|
|
22
|
-
.replace(/:.*$/, "") // :役割語 を除去
|
|
23
|
-
.trim(),
|
|
24
|
-
)
|
|
25
|
-
.filter(Boolean);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* "発売日:2014年10月25日" → "2014-10-25"
|
|
30
|
-
*/
|
|
31
|
-
function parseDateFromBodyText(bodyText: string): string | undefined {
|
|
32
|
-
const m = bodyText.match(/発売日[::]\s*(\d{4})年(\d{1,2})月(\d{1,2})日/);
|
|
33
|
-
if (!m) return undefined;
|
|
34
|
-
return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* ページ埋め込みの Colorme JSON から商品情報を取得する。
|
|
39
|
-
* `var Colorme = {...};` 形式。
|
|
40
|
-
*/
|
|
41
|
-
function extractColormeProduct(html: string): { isbn?: string; price?: number } {
|
|
42
|
-
const m = html.match(/var Colorme = (\{[^\n]+\});/);
|
|
43
|
-
if (!m) return {};
|
|
44
|
-
try {
|
|
45
|
-
const data = JSON.parse(m[1]) as {
|
|
46
|
-
product?: { model_number?: string; sales_price_including_tax?: number };
|
|
47
|
-
};
|
|
48
|
-
const product = data.product;
|
|
49
|
-
if (!product) return {};
|
|
50
|
-
const rawIsbn = product.model_number;
|
|
51
|
-
const isbn = rawIsbn ? rawIsbn.replace(/-/g, "") : undefined;
|
|
52
|
-
const price = product.sales_price_including_tax;
|
|
53
|
-
return { isbn, price };
|
|
54
|
-
} catch {
|
|
55
|
-
return {};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const rutlesAdapter: PublisherAdapter = {
|
|
60
|
-
id: "rutles",
|
|
61
|
-
name: "ラトルズ",
|
|
62
|
-
baseUrl: BASE_URL,
|
|
63
|
-
|
|
64
|
-
async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
|
|
65
|
-
const word = [query.title, query.author].filter(Boolean).join(" ");
|
|
66
|
-
if (!word) return [];
|
|
67
|
-
|
|
68
|
-
const url = `${BASE_URL}/?mode=srh&keyword=${encodeEucJp(word)}`;
|
|
69
|
-
const html = await fetchText(url, deps);
|
|
70
|
-
const doc = deps.parser.parse(html);
|
|
71
|
-
|
|
72
|
-
const results: BookRecord[] = [];
|
|
73
|
-
const limit = query.limit ?? 10;
|
|
74
|
-
|
|
75
|
-
for (const item of doc.select("li.c-item-list__item")) {
|
|
76
|
-
const linkEl = item.find("div.c-item-list__ttl a")[0];
|
|
77
|
-
if (!linkEl) continue;
|
|
78
|
-
|
|
79
|
-
const title = linkEl.text().trim();
|
|
80
|
-
const href = linkEl.attr("href");
|
|
81
|
-
if (!title || !href) continue;
|
|
82
|
-
|
|
83
|
-
// 【電子版】 が含まれるもののみ対象
|
|
84
|
-
if (!title.includes("【電子版】")) continue;
|
|
85
|
-
|
|
86
|
-
// `?pid=...` 形式の相対URLを `https://shop.rutles.net/?pid=...` に変換
|
|
87
|
-
const bookUrl = new URL(href, BASE_URL + "/").toString();
|
|
88
|
-
|
|
89
|
-
const priceText = item.find("div.c-item-list__price")[0]?.text().trim();
|
|
90
|
-
const price = priceText ? parseJapanesePrice(priceText) : undefined;
|
|
91
|
-
|
|
92
|
-
const imgEl = item.find("div.c-item-list__img img")[0];
|
|
93
|
-
const coverImageUrl = imgEl?.attr("src") ?? undefined;
|
|
94
|
-
|
|
95
|
-
results.push({
|
|
96
|
-
title,
|
|
97
|
-
authors: [],
|
|
98
|
-
publisher: "ラトルズ",
|
|
99
|
-
url: bookUrl,
|
|
100
|
-
price,
|
|
101
|
-
coverImageUrl,
|
|
102
|
-
ebookStores: [{ name: "ラトルズ", url: bookUrl, drm: "free" }],
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
if (results.length >= limit) break;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return results;
|
|
109
|
-
},
|
|
110
|
-
|
|
111
|
-
async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
|
|
112
|
-
const html = await fetchText(url, deps);
|
|
113
|
-
const doc = deps.parser.parse(html);
|
|
114
|
-
|
|
115
|
-
const getMeta = (prop: string): string | undefined =>
|
|
116
|
-
doc.selectOne(`meta[property="${prop}"]`)?.attr("content")?.trim() || undefined;
|
|
117
|
-
|
|
118
|
-
// タイトル: h2.p-product-info__ttl または og:title から " - ..." を除去
|
|
119
|
-
const h2Title = doc.selectOne("h2.p-product-info__ttl")?.text().trim();
|
|
120
|
-
const ogTitle = getMeta("og:title")?.replace(/\s*[--]\s*出版社ラトルズ公式ネットショップ.*$/, "").trim();
|
|
121
|
-
const title = h2Title || ogTitle || "";
|
|
122
|
-
|
|
123
|
-
// 説明文テキストから著者・発売日を取得
|
|
124
|
-
const bodyText = doc.selectOne("div.p-product-explain__body")?.text() ?? "";
|
|
125
|
-
const authors = parseAuthorsFromBodyText(bodyText);
|
|
126
|
-
const publishedAt = parseDateFromBodyText(bodyText);
|
|
127
|
-
|
|
128
|
-
// Colorme JSON から ISBN・価格を取得
|
|
129
|
-
const { isbn, price: colormePrice } = extractColormeProduct(html);
|
|
130
|
-
|
|
131
|
-
// og:price をフォールバックとして使用
|
|
132
|
-
const metaPriceStr = getMeta("product:price:amount");
|
|
133
|
-
const price = colormePrice ?? (metaPriceStr ? parseInt(metaPriceStr, 10) : undefined);
|
|
134
|
-
|
|
135
|
-
const coverImageUrl = getMeta("og:image") ?? undefined;
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
title,
|
|
139
|
-
authors,
|
|
140
|
-
publisher: "ラトルズ",
|
|
141
|
-
url,
|
|
142
|
-
isbn,
|
|
143
|
-
price,
|
|
144
|
-
publishedAt,
|
|
145
|
-
coverImageUrl,
|
|
146
|
-
ebookStores: [{ name: "ラトルズ", url, drm: "free" }],
|
|
147
|
-
};
|
|
148
|
-
},
|
|
149
|
-
};
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
|
|
2
|
-
import type { BookRecord, SearchQuery } from "../../domain/book.js";
|
|
3
|
-
import { fetchText, parseJapanesePrice, parseJapaneseDateToISO, stripAuthorRole } from "./base.js";
|
|
4
|
-
|
|
5
|
-
const BASE_URL = "https://www.saiensu.co.jp";
|
|
6
|
-
const SEARCH_URL = `${BASE_URL}/search/`;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* "堀井俊佑(早稲田大学准教授) 監修" → "堀井俊佑"
|
|
10
|
-
* 所属(括弧内)と役割語を除去する。
|
|
11
|
-
*/
|
|
12
|
-
function parseAuthorName(text: string): string {
|
|
13
|
-
return stripAuthorRole(text.replace(/\(.*?\)/g, ""));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** "ISBN:978-4-7819-9049-1" → "9784781990491" */
|
|
17
|
-
function parseIsbn(text: string): string | undefined {
|
|
18
|
-
const m = text.match(/[\d-]{13,}/);
|
|
19
|
-
return m ? m[0].replace(/-/g, "") : undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** "発行:サイエンス社" → "サイエンス社" */
|
|
23
|
-
function parsePublisher(text: string): string {
|
|
24
|
-
return text.replace(/^発行[::]/, "").trim();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const saiensuAdapter: PublisherAdapter = {
|
|
28
|
-
id: "saiensu",
|
|
29
|
-
name: "サイエンス社",
|
|
30
|
-
baseUrl: BASE_URL,
|
|
31
|
-
|
|
32
|
-
async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
|
|
33
|
-
const word = [query.title, query.author].filter(Boolean).join(" ");
|
|
34
|
-
if (!word) return [];
|
|
35
|
-
|
|
36
|
-
const url = `${SEARCH_URL}?keyword=${encodeURIComponent(word)}`;
|
|
37
|
-
const html = await fetchText(url, deps);
|
|
38
|
-
const doc = deps.parser.parse(html);
|
|
39
|
-
|
|
40
|
-
const results: BookRecord[] = [];
|
|
41
|
-
const limit = query.limit ?? 10;
|
|
42
|
-
|
|
43
|
-
for (const article of doc.select("article.bookListItem")) {
|
|
44
|
-
// 電子書籍のみを対象とする
|
|
45
|
-
const mediaName = article.find(".bookListItemData_mediaName")[0]?.text().trim();
|
|
46
|
-
if (mediaName !== "電子") continue;
|
|
47
|
-
|
|
48
|
-
const titleEl = article.find("h4.bookListItemData_title p.minor-mt a.link_item")[0];
|
|
49
|
-
if (!titleEl) continue;
|
|
50
|
-
|
|
51
|
-
const title = titleEl.text().trim();
|
|
52
|
-
const href = titleEl.attr("href");
|
|
53
|
-
if (!title || !href) continue;
|
|
54
|
-
|
|
55
|
-
const subtitle = article.find("small.bookListItemData_subtitle")[0]?.text().trim();
|
|
56
|
-
const fullTitle = subtitle ? `${title} ―${subtitle}` : title;
|
|
57
|
-
|
|
58
|
-
const authorText = article.find(".bookListItemData_author a.link_item")
|
|
59
|
-
.map(el => parseAuthorName(el.text()))
|
|
60
|
-
.filter(Boolean);
|
|
61
|
-
|
|
62
|
-
const priceText = article.find(".bookListItemData_priceIncludedTaxNumber")[0]?.text().trim();
|
|
63
|
-
const price = priceText ? parseJapanesePrice(priceText) : undefined;
|
|
64
|
-
|
|
65
|
-
const dateText = article.find(".bookListItemData_publishDate")[0]?.text().trim();
|
|
66
|
-
const publishedAt = dateText ? parseJapaneseDateToISO(dateText) : undefined;
|
|
67
|
-
|
|
68
|
-
const publisherText = article.find(".bookListItemData_publisher")[0]?.text().trim();
|
|
69
|
-
const publisher = publisherText ? parsePublisher(publisherText) : "サイエンス社";
|
|
70
|
-
|
|
71
|
-
const isbnText = article.find(".bookListItemData_isbn2")[0]?.text().trim();
|
|
72
|
-
const isbn = isbnText ? parseIsbn(isbnText) : undefined;
|
|
73
|
-
|
|
74
|
-
const imgEl = article.find(".bookListItemCover img")[0];
|
|
75
|
-
const coverImageUrl = imgEl?.attr("src") ?? undefined;
|
|
76
|
-
|
|
77
|
-
const bookUrl = href.startsWith("http") ? href : `${BASE_URL}${href}`;
|
|
78
|
-
|
|
79
|
-
results.push({
|
|
80
|
-
title: fullTitle,
|
|
81
|
-
authors: authorText,
|
|
82
|
-
publisher,
|
|
83
|
-
url: bookUrl,
|
|
84
|
-
isbn,
|
|
85
|
-
price,
|
|
86
|
-
publishedAt,
|
|
87
|
-
coverImageUrl,
|
|
88
|
-
// パスワード認証付きPDF販売。標準PDFビューアで閲覧可能だが技術的制限あり
|
|
89
|
-
ebookStores: [{ name: "サイエンス社", url: bookUrl, drm: "password_pdf" }],
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (results.length >= limit) break;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return results;
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
|
|
99
|
-
const html = await fetchText(url, deps);
|
|
100
|
-
const doc = deps.parser.parse(html);
|
|
101
|
-
|
|
102
|
-
const titleMain = doc.selectOne("h2.mainDetail_bookTitle")?.text().trim() ?? "";
|
|
103
|
-
const subtitle = doc.selectOne(".mainDetail_subTitle")?.text().trim();
|
|
104
|
-
const title = subtitle ? `${titleMain} ―${subtitle}` : titleMain;
|
|
105
|
-
|
|
106
|
-
const authors = doc.select(".mainDetail_authorList a.link_item")
|
|
107
|
-
.map(el => parseAuthorName(el.text()))
|
|
108
|
-
.filter(Boolean);
|
|
109
|
-
|
|
110
|
-
const priceText = doc.selectOne(".price_num")?.text().trim();
|
|
111
|
-
const price = priceText ? parseJapanesePrice(priceText) : undefined;
|
|
112
|
-
|
|
113
|
-
const dateText = doc.selectOne(".bookDetail_publishDate")?.text().trim();
|
|
114
|
-
const publishedAt = dateText ? parseJapaneseDateToISO(dateText) : undefined;
|
|
115
|
-
|
|
116
|
-
const publisherText = doc.selectOne(".bookDetail_publisher")?.text().trim();
|
|
117
|
-
const publisher = publisherText ? parsePublisher(publisherText) : "サイエンス社";
|
|
118
|
-
|
|
119
|
-
const isbnText = doc.selectOne(".bookDetail_isbn")?.text().trim();
|
|
120
|
-
const isbn = isbnText ? parseIsbn(isbnText) : undefined;
|
|
121
|
-
|
|
122
|
-
const coverImageUrl = doc.selectOne("img.mainDetail_bookImage")?.attr("src") ?? undefined;
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
title,
|
|
126
|
-
authors,
|
|
127
|
-
publisher,
|
|
128
|
-
url,
|
|
129
|
-
isbn,
|
|
130
|
-
price,
|
|
131
|
-
publishedAt,
|
|
132
|
-
coverImageUrl,
|
|
133
|
-
ebookStores: [{ name: "サイエンス社", url, drm: "password_pdf" }],
|
|
134
|
-
};
|
|
135
|
-
},
|
|
136
|
-
};
|