@zonuexe/techbook-mcp 0.2.2 → 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.
- package/CHANGELOG.md +28 -1
- package/README.md +39 -20
- package/dist/adapters/calil.d.ts +10 -0
- package/dist/adapters/calil.d.ts.map +1 -0
- package/dist/adapters/calil.js +45 -0
- package/dist/adapters/calil.js.map +1 -0
- package/dist/adapters/openbd.d.ts +57 -0
- package/dist/adapters/openbd.d.ts.map +1 -0
- package/dist/adapters/openbd.js +87 -0
- package/dist/adapters/openbd.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 +75 -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/registry.d.ts.map +1 -1
- package/dist/adapters/publishers/registry.js +4 -0
- package/dist/adapters/publishers/registry.js.map +1 -1
- package/dist/adapters/publishers/tatsu-zine.d.ts.map +1 -1
- package/dist/adapters/publishers/tatsu-zine.js +6 -18
- package/dist/adapters/publishers/tatsu-zine.js.map +1 -1
- package/dist/application/get-book-by-isbn.d.ts +13 -0
- package/dist/application/get-book-by-isbn.d.ts.map +1 -0
- package/dist/application/get-book-by-isbn.js +61 -0
- package/dist/application/get-book-by-isbn.js.map +1 -0
- package/dist/application/get-book-detail.d.ts.map +1 -1
- package/dist/application/get-book-detail.js +16 -1
- 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 +20 -0
- 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/main.js +15 -1
- package/dist/main.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +10 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +16 -0
- 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/flake.lock +61 -0
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -36
- 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/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/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 -154
- package/src/adapters/publishers/techbookfest.ts +0 -179
- package/src/application/get-book-detail.ts +0 -24
- package/src/application/search-books.ts +0 -44
- 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 -103
- package/src/mcp/tools.ts +0 -54
- 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/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/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 -22
- 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/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-detail.test.ts +0 -102
- package/tests/unit/application/search-books.test.ts +0 -137
- package/tsconfig.json +0 -17
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
|
|
2
|
-
import type { BookRecord, SearchQuery } from "../../domain/book.js";
|
|
3
|
-
import { fetchText, parseJapanesePrice, resolveUrl } from "./base.js";
|
|
4
|
-
|
|
5
|
-
const BASE_URL = "https://optronics-ebook.com";
|
|
6
|
-
const LIST_URL = `${BASE_URL}/products/list.php`;
|
|
7
|
-
|
|
8
|
-
/** 電子書籍カテゴリID */
|
|
9
|
-
const EBOOK_CATEGORY_ID = "1";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* listcomment / main_comment のテキストから各フィールドを抽出する。
|
|
13
|
-
* フォーマット例:
|
|
14
|
-
* "著者:波多腰 玄一\n発行:㈱オプトロニクス社\n頁数:384頁\n..."
|
|
15
|
-
*/
|
|
16
|
-
function parseComment(text: string): { authors: string[]; publisher?: string } {
|
|
17
|
-
const authorMatch = text.match(/著者[::]\s*([^\n<]+)/);
|
|
18
|
-
const authors = authorMatch
|
|
19
|
-
? authorMatch[1].split(/[、,]/).map(s => s.trim()).filter(Boolean)
|
|
20
|
-
: [];
|
|
21
|
-
|
|
22
|
-
const publisherMatch = text.match(/発行[::]\s*([^\n<]+)/);
|
|
23
|
-
const publisher = publisherMatch
|
|
24
|
-
? publisherMatch[1].replace(/[㈱㈲]/g, "").trim() || undefined
|
|
25
|
-
: undefined;
|
|
26
|
-
|
|
27
|
-
return { authors, publisher };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const optronicsAdapter: PublisherAdapter = {
|
|
31
|
-
id: "optronics",
|
|
32
|
-
name: "オプトロニクス社",
|
|
33
|
-
baseUrl: BASE_URL,
|
|
34
|
-
|
|
35
|
-
async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
|
|
36
|
-
const word = [query.title, query.author].filter(Boolean).join(" ");
|
|
37
|
-
if (!word) return [];
|
|
38
|
-
|
|
39
|
-
const url = `${LIST_URL}?name=${encodeURIComponent(word)}&category_id=${EBOOK_CATEGORY_ID}`;
|
|
40
|
-
const html = await fetchText(url, deps);
|
|
41
|
-
const doc = deps.parser.parse(html);
|
|
42
|
-
|
|
43
|
-
const results: BookRecord[] = [];
|
|
44
|
-
const limit = query.limit ?? 10;
|
|
45
|
-
|
|
46
|
-
for (const block of doc.select("div.list_area")) {
|
|
47
|
-
const titleEl = block.find("div.listrightbloc h3 a")[0];
|
|
48
|
-
if (!titleEl) continue;
|
|
49
|
-
|
|
50
|
-
const title = titleEl.text().trim();
|
|
51
|
-
const href = titleEl.attr("href");
|
|
52
|
-
if (!title || !href) continue;
|
|
53
|
-
|
|
54
|
-
const bookUrl = href.startsWith("http") ? href : `${BASE_URL}${href}`;
|
|
55
|
-
|
|
56
|
-
const priceText = block.find("div.pricebox .price")[0]?.text().trim();
|
|
57
|
-
const price = priceText ? parseJapanesePrice(priceText) : undefined;
|
|
58
|
-
|
|
59
|
-
const commentText = block.find("div.listcomment")[0]?.text().trim() ?? "";
|
|
60
|
-
const { authors, publisher } = parseComment(commentText);
|
|
61
|
-
|
|
62
|
-
const imgEl = block.find("div.listphoto img.picture")[0];
|
|
63
|
-
const imgSrc = imgEl?.attr("src");
|
|
64
|
-
const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
|
|
65
|
-
|
|
66
|
-
results.push({
|
|
67
|
-
title,
|
|
68
|
-
authors,
|
|
69
|
-
publisher: publisher ?? "オプトロニクス社",
|
|
70
|
-
url: bookUrl,
|
|
71
|
-
price,
|
|
72
|
-
coverImageUrl,
|
|
73
|
-
ebookStores: [{ name: "オプトロニクス社", url: bookUrl, drm: "free" }],
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (results.length >= limit) break;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return results;
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
|
|
83
|
-
const html = await fetchText(url, deps);
|
|
84
|
-
const doc = deps.parser.parse(html);
|
|
85
|
-
|
|
86
|
-
// タイトル: <title>OPTRONICS eBOOK / {title}</title>
|
|
87
|
-
const rawTitle = doc.selectOne("title")?.text().trim() ?? "";
|
|
88
|
-
const title = rawTitle.replace(/^OPTRONICS\s+eBOOK\s*[//]\s*/, "").trim();
|
|
89
|
-
|
|
90
|
-
// 価格: #price02_default
|
|
91
|
-
const priceText = doc.selectOne("#price02_default")?.text().trim();
|
|
92
|
-
const price = priceText ? parseJapanesePrice(priceText) : undefined;
|
|
93
|
-
|
|
94
|
-
// 著者・発行元: div.main_comment テキスト
|
|
95
|
-
const commentText = doc.selectOne("div.main_comment")?.text().trim() ?? "";
|
|
96
|
-
const { authors, publisher } = parseComment(commentText);
|
|
97
|
-
|
|
98
|
-
// カバー画像: img.picture
|
|
99
|
-
const imgEl = doc.selectOne("img.picture");
|
|
100
|
-
const imgSrc = imgEl?.attr("src");
|
|
101
|
-
const coverImageUrl = imgSrc ? resolveUrl(BASE_URL, imgSrc) : undefined;
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
title,
|
|
105
|
-
authors,
|
|
106
|
-
publisher: publisher ?? "オプトロニクス社",
|
|
107
|
-
url,
|
|
108
|
-
price,
|
|
109
|
-
coverImageUrl,
|
|
110
|
-
ebookStores: [{ name: "オプトロニクス社", url, drm: "free" }],
|
|
111
|
-
};
|
|
112
|
-
},
|
|
113
|
-
};
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import type { PublisherAdapter, PublisherDeps } from "../../domain/publisher.js";
|
|
2
|
-
import type { BookRecord, SearchQuery } from "../../domain/book.js";
|
|
3
|
-
import { fetchText, parseJapanesePrice, resolveUrl, parseJapaneseDateToISO, stripAuthorRole } from "./base.js";
|
|
4
|
-
|
|
5
|
-
const BASE_URL = "https://www.oreilly.co.jp";
|
|
6
|
-
const EBOOK_LIST_URL = `${BASE_URL}/ebook/`;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* "2026年04月03日" → "2026-04-03"
|
|
10
|
-
* "2025-04-08" (content属性) はそのまま返す
|
|
11
|
-
*/
|
|
12
|
-
function parseOreillyDate(text: string): string | undefined {
|
|
13
|
-
const isoMatch = text.match(/\d{4}-\d{2}-\d{2}/);
|
|
14
|
-
if (isoMatch) return isoMatch[0];
|
|
15
|
-
return parseJapaneseDateToISO(text);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 著者文字列をパースして配列に変換する。
|
|
20
|
-
* 例: "Dan Vanderkam 著、今村 謙士 訳" → ["Dan Vanderkam", "今村 謙士"]
|
|
21
|
-
*/
|
|
22
|
-
function parseAuthors(text: string): string[] {
|
|
23
|
-
return text.split(/[、,]/).map(stripAuthorRole).filter(Boolean);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const oreillyJapanAdapter: PublisherAdapter = {
|
|
27
|
-
id: "oreilly-japan",
|
|
28
|
-
name: "オライリー・ジャパン",
|
|
29
|
-
baseUrl: BASE_URL,
|
|
30
|
-
|
|
31
|
-
async search(query: SearchQuery, deps: PublisherDeps): Promise<BookRecord[]> {
|
|
32
|
-
// 検索APIがないためタイトルでローカルフィルタリングする
|
|
33
|
-
// 著者のみの検索は各書籍詳細ページを全取得しないと不可能なため非対応
|
|
34
|
-
if (!query.title) return [];
|
|
35
|
-
|
|
36
|
-
const keyword = query.title.toLowerCase();
|
|
37
|
-
const limit = query.limit ?? 10;
|
|
38
|
-
|
|
39
|
-
const html = await fetchText(EBOOK_LIST_URL, deps);
|
|
40
|
-
const doc = deps.parser.parse(html);
|
|
41
|
-
|
|
42
|
-
const results: BookRecord[] = [];
|
|
43
|
-
|
|
44
|
-
for (const row of doc.select(".ebookCatalog tbody tr")) {
|
|
45
|
-
const titleEl = row.find("td.title a")[0];
|
|
46
|
-
if (!titleEl) continue;
|
|
47
|
-
|
|
48
|
-
const title = titleEl.text().trim();
|
|
49
|
-
if (!title.toLowerCase().includes(keyword)) continue;
|
|
50
|
-
|
|
51
|
-
const href = titleEl.attr("href");
|
|
52
|
-
if (!href) continue;
|
|
53
|
-
const url = resolveUrl(EBOOK_LIST_URL, href);
|
|
54
|
-
|
|
55
|
-
const isbnRaw = row.find("td.isbn")[0]?.text().trim();
|
|
56
|
-
const isbn = isbnRaw?.replace(/-/g, "");
|
|
57
|
-
|
|
58
|
-
const priceText = row.find("td.price")[0]?.text().trim();
|
|
59
|
-
const price = priceText ? parseJapanesePrice(priceText) : undefined;
|
|
60
|
-
|
|
61
|
-
const dateText = row.find("td")[3]?.text().trim();
|
|
62
|
-
const publishedAt = dateText ? parseOreillyDate(dateText) : undefined;
|
|
63
|
-
|
|
64
|
-
const coverImageUrl = isbn
|
|
65
|
-
? `${BASE_URL}/books/images/picture_large${isbnRaw}.jpeg`
|
|
66
|
-
: undefined;
|
|
67
|
-
|
|
68
|
-
results.push({
|
|
69
|
-
title,
|
|
70
|
-
authors: [],
|
|
71
|
-
publisher: "オライリー・ジャパン",
|
|
72
|
-
url,
|
|
73
|
-
isbn,
|
|
74
|
-
price,
|
|
75
|
-
publishedAt,
|
|
76
|
-
coverImageUrl,
|
|
77
|
-
ebookStores: [{ name: "オライリー・ジャパン", url, drm: "free" }],
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (results.length >= limit) break;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return results;
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
async getDetail(url: string, deps: PublisherDeps): Promise<BookRecord> {
|
|
87
|
-
const html = await fetchText(url, deps);
|
|
88
|
-
const doc = deps.parser.parse(html);
|
|
89
|
-
|
|
90
|
-
const titleMain = doc.selectOne("h1[itemprop='name']")?.text().trim() ?? "";
|
|
91
|
-
const subTitle = doc.selectOne("p.sub_title")?.text()
|
|
92
|
-
.replace(/^[\s\u3000―-]+/, "").trim();
|
|
93
|
-
const title = subTitle ? `${titleMain} ―${subTitle}` : titleMain;
|
|
94
|
-
|
|
95
|
-
const authorText = doc.selectOne("span[itemprop='author']")?.text().trim() ?? "";
|
|
96
|
-
const authors = parseAuthors(authorText);
|
|
97
|
-
|
|
98
|
-
const isbnRaw = doc.selectOne("dd[itemprop='isbn']")?.text().trim();
|
|
99
|
-
const isbn = isbnRaw?.replace(/-/g, "");
|
|
100
|
-
|
|
101
|
-
const publishedAt = doc.selectOne("dd[itemprop='datePublished']")?.attr("content")
|
|
102
|
-
?? undefined;
|
|
103
|
-
|
|
104
|
-
const coverImageUrl = doc.selectOne("img.cover-photo")?.attr("src") ?? undefined;
|
|
105
|
-
|
|
106
|
-
const description = doc.selectOne("p[itemprop='description']")?.text().trim()
|
|
107
|
-
|| undefined;
|
|
108
|
-
|
|
109
|
-
// Ebook価格を取得: "Ebook" という option-name の次の div
|
|
110
|
-
let price: number | undefined;
|
|
111
|
-
for (const item of doc.select(".option-item")) {
|
|
112
|
-
const name = item.find(".option-name")[0]?.text().trim();
|
|
113
|
-
if (name === "Ebook") {
|
|
114
|
-
const priceText = item.find("div")[1]?.text().trim();
|
|
115
|
-
if (priceText) price = parseJapanesePrice(priceText);
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
title,
|
|
122
|
-
authors,
|
|
123
|
-
publisher: "オライリー・ジャパン",
|
|
124
|
-
url,
|
|
125
|
-
isbn,
|
|
126
|
-
price,
|
|
127
|
-
publishedAt,
|
|
128
|
-
description,
|
|
129
|
-
coverImageUrl,
|
|
130
|
-
ebookStores: [{ name: "オライリー・ジャパン", url, drm: "free" }],
|
|
131
|
-
};
|
|
132
|
-
},
|
|
133
|
-
};
|
|
@@ -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
|
-
];
|