1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AT Protocol初心者がアニメの視聴状況をPDSに保存できるツールを作ってみた

Last updated at Posted at 2024-12-16

はじめに

この記事は、Bluesky Advent Calendar 2024 16日目の記事です。

はじめまして、marilと申します。

今回は、ユーザーのPDSにアニメの視聴状況を保存できるツール「AniBlue」のベータ版としてひとまず動くものができたので、ご紹介させていただきます。

AT Protocolに関してまだまだ初心者なので、間違いや改善点等ありましたらご指摘いただけると助かります。

作ったもの

GitHub

「AniBlue」は、アニメの視聴ステータス/お気に入りなどの情報を、すべてユーザーのPDSに保存することができるアニメ視聴管理ツールです。

アニメのタイトルやエピソードなどの情報の取得には、Annict APIを使用させていただきました。

機能

選択したアニメを

  • 見たい
  • 視聴中
  • 視聴済み
  • お気に入り

の状態に設定して、一覧で管理することができます。

また、画像のように視聴しているエピソードを記録することもできます。

image.png

Lexicon

アニメの視聴ステータスは、以下のようなlexiconで配列として保存されます。

{
  "lexicon": 1,
  "id": "app.netlify.aniblue.status",
  "defs": {
    "main": {
      "type": "record",
      "description": "A record that stores the status of the anime.",
      "key": "literal:self",
      "record": {
        "type": "object",
        "required": ["status"],
        "properties": {
          "status": {
            "type": "array",
            "items": {
              "type": "ref",
              "ref": "#status"
            }
          }
        }
      }
    },
    "status": {
      "type": "object",
      "required": ["id", "title", "status", "episode_text"],
      "properties": {
        "id": {
          "type": "integer",
          "description": "Annict API ID for the anime"
        },
        "title": {
          "type": "string",
          "description": "Title of the anime"
        },
        "thumbnail": {
          "type": "string",
          "description": "URL of the anime thumbnail image"
        },
        "status": {
          "type": "string",
          "description": "Current watching status of the anime",
          "enum": ["watching", "watched", "pending"]
        },
        "episode_text": {
          "type": "string",
          "description": "Current number text of episode"
        },
        "favorite": {
          "type": "boolean",
          "description": "Favorite flag of the anime"
        }
      }
    }
  }
}

例えば、私のアカウントに登録されているレコードは、このようになっています。

このレコードを取得、recoilのatomとして定義します。

export default function WorkDetail() {
  const { work, status, error } = useLoaderData<typeof loader>();

  const setAnimeState = useSetAnimeState();

  if (status) {
    setAnimeState(status.value.status);
  } else {
    setAnimeState([]);
  }

先にstateを更新して、そのstateをもとにPDS側も更新しています。

    const updateAnimeState = async (newState: AnimeStatus[]) => {
      // ローカルのstateを更新
      setAnimeState(newState);

      // PDS側の更新
      const res = await fetch("/api/status/update", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          $type: "app.netlify.aniblue.status",
          status: newState,
        }),
      });

      const json = await res.json();

      if (!json.ok) {
        throw new Error("情報の更新に失敗しました");
      }
    };

    const handleStatusUpdate = async (status: Status) => {
      try {
        // prevStateが存在しない場合はレコードを新規作成
        const newState = prevState
          ? animeState.map((item) =>
              item.id === id ? { ...item, status } : item
            )
          : [
              ...animeState,
              {
                id,
                title,
                thumbnail: imageUrl,
                status,
                episode_text: "",
                favorite: false,
              },
            ];

        await updateAnimeState(newState);
import { ActionFunction } from "@remix-run/node";
import {
  isRecord,
  validateRecord,
} from "~/generated/api/types/app/netlify/aniblue/status";
import { StatusAgent } from "~/lib/agent/statusAgent";
import { getSessionAgent } from "~/lib/auth/session";

export const action: ActionFunction = async ({ request }) => {
  const agent = await getSessionAgent(request);
  if (agent == null) return new Response(null, { status: 401 });

  const record = await request.json();

  const statusAgent = new StatusAgent(agent);

  //バリデーション
  if (isRecord(record) && validateRecord(record)) {
    await statusAgent.put(record, agent.assertDid);

    return { ok: true };
  }

  return new Response(null, { status: 500 });
};

認証

OAuthでの認証周りは、以下の記事を参考にして実装しました。

ただ、RemixとVercelの相性問題なのか、VercelにデプロイするとOAuthResolverErrorが発生してしまったため、代替としてNetlifyにデプロイしています。

(issueによると、RemixのSingle Fetchに関連する問題っぽい?)

課題

  • self レコードに配列で管理しているだけなので、ステータスの登録数が増えてくるとパフォーマンスに影響が出そう
    FirehoseからDBを更新して管理するのが正解?

おわりに

最後まで読んでくださり、ありがとうございました。

「AT Protocol、なんか面白そう!」という思い付きと勢い、アドカレまでに何か作りたいという気持ちだけで作ったアプリでしたが、沢山の方に使っていただけて感謝しています。

改善点などありましたら、お気軽にコメントや@maril445.bsky.socialまでご連絡ください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?