summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.claude/skills/update-band-info.md163
-rw-r--r--app/root.tsx10
-rw-r--r--app/routes.ts2
-rw-r--r--app/routes/api-artist-detail.tsx49
-rw-r--r--app/routes/api-band-detail.tsx69
5 files changed, 287 insertions, 6 deletions
diff --git a/.claude/skills/update-band-info.md b/.claude/skills/update-band-info.md
new file mode 100644
index 0000000..cb1c63c
--- /dev/null
+++ b/.claude/skills/update-band-info.md
@@ -0,0 +1,163 @@
+---
+description: バンド名を受け取り、公式SNSから説明文・メンバー情報を取得して whoisband.yyamashita.com の本番APIを更新する
+---
+
+# バンド情報最新化スキル
+
+本番API(`https://whoisband.yyamashita.com`)の既存バンド情報を SNS プロフィールから最新化する。
+
+## 使い方
+
+```
+/update-band-info BAND_NAME
+```
+
+例: `/update-band-info 東京事変`
+
+---
+
+## 手順
+
+### Step 1: バンドを特定する
+
+```bash
+curl -s "https://whoisband.yyamashita.com/api/bands" | python3 -c "
+import json, sys
+data = json.load(sys.stdin)
+name = '$ARGUMENTS'.strip()
+matches = [b for b in data if name.lower() in b['name'].lower() or name.lower() in b['slug'].lower()]
+print(json.dumps(matches, ensure_ascii=False, indent=2))
+"
+```
+
+- 一致なし → ユーザーに「登録されていません。`/register-band` で登録してください」と伝えて終了
+- 複数一致 → 候補を列挙してユーザーに選択を求める
+- 1件一致 → UUID を控えて次へ
+
+### Step 2: バンドの現在データを取得する
+
+```bash
+curl -s "https://whoisband.yyamashita.com/api/bands/{uuid}"
+```
+
+レスポンスから以下を確認:
+- `links`: 既存の SNS リンク一覧(URL と label)
+- `members`: 既存メンバー一覧(`artist_id`, `artist_name`)
+- `description`: 現在の説明文
+
+### Step 3: SNS プロフィールを取得する
+
+既存リンクの中から SNS アカウントを優先順位順に処理する:
+
+**優先順位**: X (twitter.com/x.com) → Instagram → その他
+
+SNS リンクが存在しない場合は WebSearch で探す:
+- `{バンド名} x.com site:x.com`
+- `{バンド名} instagram.com`
+
+各 SNS について WebFetch でプロフィールページを取得し、以下を抽出:
+
+| 取得対象 | 内容 |
+|---|---|
+| **説明文** | アカウントの bio / 自己紹介文 |
+| **フォロワー数・投稿数** | 活動の活発さの目安(参考情報) |
+| **メンバー名** | bio や固定投稿に名前が書いてある場合 |
+| **新規 SNS リンク** | 他 SNS へのリンクが bio に記載されていれば収集 |
+
+### Step 4: 説明文を作成する
+
+収集したバイオから日本語の説明文を生成する:
+
+- 200文字以内
+- 箇条書きではなく文章形式
+- 「〜のバンド」「〜を拠点に活動」など簡潔に
+- 英語バイオの場合は日本語に意訳する(直訳不要)
+- 既存の `description` が十分な情報量なら上書きしない判断も可
+
+### Step 5: メンバーを照合・登録する
+
+メンバー情報が SNS から読み取れた場合:
+
+**5-1. 既存アーティストとの照合**
+
+```bash
+curl -s "https://whoisband.yyamashita.com/api/artists"
+```
+
+照合ルール(以下の順に判定):
+
+1. **SNS URL が一致** → 同一アーティストと断定
+ - 既存アーティストのリンク一覧と比較
+ - `curl -s "https://whoisband.yyamashita.com/api/artists/{uuid}"` で各アーティストのリンクを確認
+2. **名前が完全一致** → 同一アーティストと断定
+3. **名前が部分一致または表記ゆれ** → スキップ(あいまいな場合は追加しない)
+4. **一致なし** → 新規アーティストとして登録
+
+**5-2. 新規アーティストの登録**
+
+```bash
+curl -s -X POST "https://whoisband.yyamashita.com/api/artists" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "メンバー名",
+ "links": [
+ { "label": "x", "url": "https://x.com/..." }
+ ],
+ "message": "SNS情報から自動登録"
+ }'
+```
+
+レスポンスの `id` (UUID) を控える。
+
+**5-3. メンバーの担当パートを特定する**
+
+bio / 固定投稿 / プロフィール情報からパートを読み取る。
+明記されていない場合は `"Other"` を使う。
+
+使えるロール: `Vocal`, `Guitar`, `Bass`, `Drums`, `Keyboard`, `DJ`, `Strings`, `Brass`, `Percussion`, `Programming`, `Manipulator`, `Turntable`, `Other`
+
+### Step 6: バンドを更新する
+
+```bash
+curl -s -X PATCH "https://whoisband.yyamashita.com/api/bands/{uuid}" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "description": "SNSから取得した説明文",
+ "append_links": true,
+ "links": [
+ { "label": "x", "url": "https://x.com/bandaccount" },
+ { "label": "instagram", "url": "https://www.instagram.com/bandaccount/" }
+ ],
+ "append_members": true,
+ "members": [
+ { "artist_id": "uuid-of-member", "role": "Vocal", "since": "", "until": "", "note": "" }
+ ],
+ "message": "SNS情報から自動更新"
+ }'
+```
+
+- `append_links: true` → 既存リンクに追記(重複 URL は自動スキップ)
+- `append_members: true` → 既存メンバーに追記(既存の artist_id は自動スキップ)
+- 説明文のみ更新したい場合は `links: []`, `members: []` にする
+
+### Step 7: 結果を報告する
+
+```
+更新完了: バンド名
+→ https://whoisband.yyamashita.com/bands/of/{uuid}
+
+更新内容:
+- 説明文: 「...」
+- 追加リンク: x.com/... など
+- 追加メンバー: 名前(担当)× N 名
+```
+
+---
+
+## 注意事項
+
+- SNS が非公開 / 存在しない場合はその旨を伝えてスキップ
+- description が取得できなかった場合は description の更新をスキップ(空文字で上書きしない)
+- メンバーが全員既存登録済みの場合は `members: []` で送信(重複しない)
+- X(Twitter) のプロフィール取得は `https://x.com/{handle}` ではなくWebSearchの結果から取得することが多い
+- Instagram は `https://www.instagram.com/{handle}/` でWebFetch可能
diff --git a/app/root.tsx b/app/root.tsx
index e30abe8..15cc68b 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -46,12 +46,10 @@ export function Layout({ children }: { children: React.ReactNode }) {
export default function App() {
return (
<>
- <nav>
- <div>
- <Link to="/" className="logo">whois.band</Link>
- <Link to="/bands">Bands</Link>
- <Link to="/artists">Artists</Link>
- </div>
+ <nav className="nav">
+ <Link to="/" className="nav-brand">whois.band</Link>
+ <Link to="/bands">Bands</Link>
+ <Link to="/artists">Artists</Link>
</nav>
<Outlet />
</>
diff --git a/app/routes.ts b/app/routes.ts
index 0a2c028..518492f 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -3,7 +3,9 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("/api/bands", "routes/api-bands.tsx"),
+ route("/api/bands/:uuid", "routes/api-band-detail.tsx"),
route("/api/artists", "routes/api-artists.tsx"),
+ route("/api/artists/:uuid", "routes/api-artist-detail.tsx"),
route("/api/export", "routes/api-export.tsx"),
route("/api/import", "routes/api-import.tsx"),
route("/bands", "routes/band-index.tsx"),
diff --git a/app/routes/api-artist-detail.tsx b/app/routes/api-artist-detail.tsx
new file mode 100644
index 0000000..1d874ad
--- /dev/null
+++ b/app/routes/api-artist-detail.tsx
@@ -0,0 +1,49 @@
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
+import {
+ getArtistById,
+ getArtistLinks,
+ getIpAddress,
+ updateArtist,
+} from "~/lib/db.server";
+
+export function loader({ params }: LoaderFunctionArgs) {
+ const artist = getArtistById(params.uuid!);
+ if (!artist) return Response.json({ error: "Not found" }, { status: 404 });
+ const links = getArtistLinks(artist.id);
+ return Response.json({ ...artist, links });
+}
+
+export async function action({ request, params }: ActionFunctionArgs) {
+ if (request.method !== "PATCH") {
+ return Response.json({ error: "Method not allowed" }, { status: 405 });
+ }
+
+ const artist = getArtistById(params.uuid!);
+ if (!artist) return Response.json({ error: "Not found" }, { status: 404 });
+
+ let body: Record<string, unknown>;
+ try {
+ body = await request.json();
+ } catch {
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
+ }
+
+ const currentLinks = getArtistLinks(artist.id);
+ const patchLinks = (body.links as { label: string; url: string }[] | undefined) ?? [];
+ const appendLinks = body.append_links !== false;
+
+ const existingUrls = new Set(currentLinks.map((l) => l.url));
+ const newLinks = appendLinks
+ ? [...currentLinks.map((l) => ({ label: l.label, url: l.url })), ...patchLinks.filter((l) => !existingUrls.has(l.url))]
+ : patchLinks;
+
+ updateArtist(artist.id, {
+ slug: (body.slug as string | undefined) ?? artist.slug,
+ name: (body.name as string | undefined) ?? artist.name,
+ links: newLinks,
+ message: (body.message as string | undefined) || "API update",
+ ip_address: getIpAddress(request),
+ });
+
+ return Response.json(getArtistById(artist.id));
+}
diff --git a/app/routes/api-band-detail.tsx b/app/routes/api-band-detail.tsx
new file mode 100644
index 0000000..aa3b5bb
--- /dev/null
+++ b/app/routes/api-band-detail.tsx
@@ -0,0 +1,69 @@
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
+import {
+ getBandById,
+ getBandLinks,
+ getBandMembers,
+ getIpAddress,
+ toSlug,
+ updateBand,
+ type MemberInput,
+} from "~/lib/db.server";
+
+export function loader({ params }: LoaderFunctionArgs) {
+ const band = getBandById(params.uuid!);
+ if (!band) return Response.json({ error: "Not found" }, { status: 404 });
+ const links = getBandLinks(band.id);
+ const members = getBandMembers(band.id);
+ return Response.json({ ...band, links, members });
+}
+
+export async function action({ request, params }: ActionFunctionArgs) {
+ if (request.method !== "PATCH") {
+ return Response.json({ error: "Method not allowed" }, { status: 405 });
+ }
+
+ const band = getBandById(params.uuid!);
+ if (!band) return Response.json({ error: "Not found" }, { status: 404 });
+
+ let body: Record<string, unknown>;
+ try {
+ body = await request.json();
+ } catch {
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
+ }
+
+ const currentLinks = getBandLinks(band.id);
+ const currentMembers = getBandMembers(band.id);
+
+ const patchLinks = (body.links as { label: string; url: string }[] | undefined) ?? [];
+ const patchMembers = (body.members as MemberInput[] | undefined) ?? [];
+ const appendLinks = body.append_links !== false;
+ const appendMembers = body.append_members !== false;
+
+ const existingUrls = new Set(currentLinks.map((l) => l.url));
+ const newLinks = appendLinks
+ ? [...currentLinks.map((l) => ({ label: l.label, url: l.url })), ...patchLinks.filter((l) => !existingUrls.has(l.url))]
+ : patchLinks;
+
+ const existingArtistIds = new Set(currentMembers.map((m) => m.artist_id));
+ const newMembers: MemberInput[] = appendMembers
+ ? [
+ ...currentMembers.map((m) => ({ artist_id: m.artist_id, role: m.role, since: m.since ?? "", until: m.until ?? "", note: m.note ?? "" })),
+ ...patchMembers.filter((m) => !existingArtistIds.has(m.artist_id)),
+ ]
+ : patchMembers;
+
+ updateBand(band.id, {
+ slug: (body.slug as string | undefined) ?? band.slug,
+ name: (body.name as string | undefined) ?? band.name,
+ area: (body.area as string | undefined) ?? band.area,
+ description: "description" in body ? (body.description as string | null) : band.description,
+ status: (body.status as string | undefined) ?? band.status,
+ links: newLinks,
+ members: newMembers,
+ message: (body.message as string | undefined) || "API update",
+ ip_address: getIpAddress(request),
+ });
+
+ return Response.json(getBandById(band.id));
+}