connector-tmdb について
connector-tmdbは、Internet Computerのcanisterから映画やテレビのカタログ情報を読む開発者向けのMotokoバインディングです。検索、詳細、トレンド、ディスカバーなどの読み取りに対応しますが、多くのリクエストボディ付き書き込みと映画推薦は利用できないとREADMEに記載されています。
主な機能
- 01TMDb API向けMotokoバインディング
- 02映画、テレビ、人物の検索
- 03詳細、クレジット、メディア情報の取得
- 04トレンド、ディスカバー、設定情報の取得
- 05v4 bearer token認証
活用シーン
- 01映画やシリーズの検索ページの構築
- 02作品詳細、クレジット、画像の表示
- 03トレンド、上映中、条件指定の一覧作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill connector-tmdbインストール後、Claude Code で/connector-tmdbを入力して読み込まれたか確認します。
詳細
tmdb-client で映画・テレビ番組のデータを扱う
The Movie Database API の Motoko バインディングです。TMDb の OpenAPI 仕様から生成されています。1つのモジュール(Apis/DefaultApi)に124個の操作が含まれています。仕様にはタグがないため、分割する対象はなく、ネストされたファサードもありません。
アプリの計画を立てる前にお読みください。読み取り機能は動作しますが、書き込み機能は動作しません。 124個の操作のうち、111個は使用可能で、13個は壊れています。
- リクエストボディを持つ操作は12個あります。そのうちPOSTの11個と
authenticationDeleteSessionは、二重にラップされて送信されます(§6)。 - 読み取り操作のうち1つ、
movieRecommendationsは、成功したレスポンスを受け取るたびに例外をスローします(§6)。
カタログの読み取り(検索、詳細、トレンド、ディスカバー、現在上映中)を中心に機能を計画し、ジェネレーターを先に修正する準備がない限り、お気に入り、評価、ウォッチリストは対象外にしてください。「類似作品」の列が必要な場合は tvSeriesRecommendations を使ってください。映画版は壊れています。
バックエンド
1. 依存関係を追加する
mops add [email protected]
2. 認証情報
v4 読み取りアクセストークンと ?#bearer を使用してください。このクライアントがサポートしている認証情報はこれだけです。
auth = ?#bearer "eyJhbGciOi…" // v4 read-access token, themoviedb.org/settings/api
TMDb は、通常 ?api_key=… クエリパラメーターとして送信するv3 APIキー(16進数32文字)も発行しています。このクライアントはこれを送信できません。 仕様で宣言されているセキュリティスキームは1つだけです。Authorization という名前の ヘッダー 内の apiKey で、x-bearer-format: bearer が指定されています。そのため、生成された操作が api_key をURLに追加することはありません(DefaultApi.mo 内に api_key は一度も登場しません)。
また、Config.Auth が提供する ?#apiKey も使わないでください。この方式では Bearer 接頭辞なしで Authorization: <key> が送信されるため、TMDb は401で拒否します。これは汎用ジェネレーターの方式で、このAPIには適していません。正しく接続されているのは ?#bearer です。
v4トークンはオフチェーンで発行され、有効期限はありません。読み取り機能にOAuthフローやトークンの更新処理を実装する必要はありません。
認証情報の保存場所
§3のアクターは、例を自己完結させるため、トークンをパラメーターとして受け取ります。実際のアプリでは、フロントエンドに認証情報を持たせてはいけません。アクターの状態に保存し、認可された更新呼び出しで設定してください。
- フィールドの型だけを宣言してください。 Caffeineアクターはマイグレーションチェーンから安定した状態を取得するため、初期化子なしの
var tmdbToken : Text;を使います。実際のプロジェクトでvar tmdbToken : Text = "";をコンパイルすると、M0250が発生します。 - セッターを
MixinAuthorizationで保護し、Configの構築時にアクター内でのみフィールドを読み取ってください。 - クエリメソッドから返したり、ログに記録したり、呼び出し元に見えるエラーに含めたりしないでください。
connector-googlemail は、この構成の実例です。型のみを宣言した var フィールドの gmailConfig レコード、セッターに適用する認可ミックスイン、初期値を提供するマイグレーションヘッドが含まれています。管理者が認証情報を管理する必要がある場合は、これを参考にしてください。プロトタイプの場合や、呼び出し元がすでに信頼できる場合は、上記のパラメーター形式で問題ありません。
3. 標準的な構成
以下のアクターは、映画検索機能のバックエンド全体です。TMDb の幅広いオプショナルなレスポンスを、フロントエンドが14個ものフィールドをnullチェックせずに扱える、必要最小限のレコードに変換します。
import Tmdb "mo:tmdb-client/Apis/DefaultApi";
import { defaultConfig; type Config } "mo:tmdb-client/Config";
import Array "mo:core/Array";
actor {
/// Narrow, frontend-friendly shape. TMDb marks almost every field
/// optional, so collapse the optionals here once instead of in the UI.
public type Movie = {
id : Int;
title : Text;
overview : Text;
releaseDate : Text;
posterUrl : Text;
voteAverage : Float;
};
/// The v4 read-access token is passed in rather than stored here — see
/// "Where the credential lives" below for the admin-set variant.
func config(token : Text) : Config = {
defaultConfig with
auth = ?#bearer token;
// search and discover payloads run large; the default cap is too
// small for a 20-result page with overviews.
max_response_bytes = ?300_000;
};
/// Poster paths come back relative (`/abc.jpg`); prepend an image base.
/// w500 is the usual card size. An empty path yields an empty string so
/// the frontend can fall back to a placeholder.
func posterUrl(path : ?Text) : Text {
switch (path) {
case (?p) "https://image.tmdb.org/t/p/w500" # p;
case null "";
};
};
/// One mapper for every list endpoint. The parameter type is written
/// STRUCTURALLY rather than naming a generated model, because each
/// endpoint has its own `…ResultsInner` type with the same fields —
/// `SearchMovie200ResponseResultsInner`,
/// `MovieNowPlayingList200ResponseResultsInner`, and so on. One structural
/// signature accepts all of them.
func toMovie(
m : {
id : ?Int;
title : ?Text;
overview : ?Text;
release_date : ?Text;
poster_path : ?Text;
vote_average : ?Float;
}
) : Movie = {
id = m.id ?? 0;
title = m.title ?? "";
overview = m.overview ?? "";
releaseDate = m.release_date ?? "";
posterUrl = posterUrl(m.poster_path);
voteAverage = m.vote_average ?? 0.0;
};
// `query` is a reserved word in Motoko — hence `term` here, and `query_`
// in the generated signature.
public func searchMovies(token : Text, term : Text, page : Int) : async [Movie] {
// searchMovie(config, query_, includeAdult, language,
// primaryReleaseYear, page, region_, year)
// Text and Bool parameters are positional and NOT optional: pass ""
// and false to omit them. Only `page` is 1-indexed.
let response = await* Tmdb.searchMovie(
config(token),
term,
false,
"en-US",
"",
page,
"",
"",
);
let results = switch (response.results) {
case (?r) r;
case null [];
};
Array.map(results, toMovie);
};
/// Full detail for one title. `appendToResponse` embeds sub-resources in
/// the SAME outcall — "credits,images,videos" costs one call, not four.
public func movieOverview(token : Text, movieId : Int) : async Text {
let movie = await* Tmdb.movieDetails(config(token), movieId, "", "en-US");
movie.overview ?? "";
};
/// "In cinemas now" feed, for a landing page.
public func nowPlaying(token : Text) : async [Movie] {
let response = await* Tmdb.movieNowPlayingList(config(token), "en-US", 1, "US");
let results = switch (response.results) {
case (?r) r;
case null [];
};
Array.map(results, toMovie);
};
}
4. 読み取りサーフェス
124 個の操作はすべて、mo:tmdb-client/Apis/DefaultApi にフリーファンクションとして定義されており、最初の引数に Config を取ります。生成される名前は、TMDb のハイフン区切りの operationId をキャメルケースにしたものです(movie-now-playing-list → movieNowPlayingList)。便利なグループは次のとおりです。
| グループ | 代表的な操作 | 用途 |
|---|---|---|
| 検索 | searchMovie、searchTv、searchPerson、searchMulti | 検索ボックス。searchMulti は映画・TV・人物を 1 回の呼び出しで返します |
| 映画の詳細 | movieDetails、movieCredits、movieImages、movieVideos、movieSimilar、movieReleaseDates | 作品ページ。movieRecommendations は使いません — §6 を参照 |
| TV の詳細 | tvSeriesDetails、tvSeasonDetails、tvEpisodeDetails、tvSeriesCredits、tvSeriesRecommendations | シリーズページ。シーズンからエピソードへ掘り下げます |
| 人物 | personDetails、personMovieCredits、personTvCredits、personImages | キャスト/スタッフのページ |
| フィード | movieNowPlayingList、movieUpcomingList、moviePopularList、movieTopRatedList、tvSeriesPopularList | ランディングページ、カルーセル |
| トレンド | trendingMovies、trendingTv、trendingPeople、trendingAll | 「今話題」の一覧。timeWindow 列挙型(#day / #week)を パス パラメーターとして取るため、裸の値であり、?T ではありません |
| レコメンデーション | tvSeriesRecommendations は動作します。movieRecommendations は動作しません — §6 を参照 | TV のみの「この作品が好きならこちらも」 |
| ディスカバー | discoverMovie、discoverTv | ジャンル、年、評価、言語、並べ替えで絞り込むブラウズ |
| アカウントの読み取り | accountGetFavorites、accountWatchlistMovies、accountWatchlistTv、accountRatedMovies、accountRatedTv、accountLists、accountDetails | ユーザー自身のリストの読み取り。正しく生成されていますが、現在取得できない sessionId が必要です — §6 を参照。accountGetWatchlist はありません。映画と TV は別々の操作です |
| 設定 | configurationDetails、configurationLanguages、configurationCountries | 画像のベース URL、言語コード。一度取得してキャッシュしてください。変更頻度は低いです |
TMDb はフィールドを null にせず省略するため、レスポンス内のすべての値はオプショナル(?T)です。§3 と同様に、アクターの境界でオプショナルをまとめて処理してください。
並べ替えパラメーターは文字列ではなく、型付きバリアントです。生成された *SortByParameter モジュールから選択してください(例:#created_at_desc)。任意の文字列を渡すと、型チェックに失敗します。
5. 呼び出しはデフォルトで非複製です
defaultConfig では is_replicated = ?false が設定されているため、各外部呼び出しは 1 つのノードで実行され、レスポンスはコンセンサスにかけられません。ここでは、読み取り・書き込みのどちらにおいてもこれが適切です。また、null(=複製あり)を設定していた 0.1.1 からの 変更点 です。
- 読み取り — カタログのレスポンスは安定しているため、複製しても利点はなく、サイクルコストが約 13 倍になります。
- 書き込み —
listCreateと 3 つのauthenticationCreateSession*操作は 冪等ではありません。複製すると、各呼び出しで約 13 個のリストまたは約 13 個のセッションが作成され、認証情報がすべてのレプリカに渡ります。
以前のガイダンスの訂正。
[email protected]に同梱されていたパッケージ内のSKILL.mdには、次のように記載されていました。「ミューテーションではis_replicated = nullのままにしてください — コンセンサスによる複製で、単一ノードの改ざんを防ぎます。」 これは逆です。コンセンサスは外部呼び出しの重複を排除するのではなく、呼び出しを増やします。冪等でない書き込みでは、複製は保護策ではなく不具合の原因です。既存コードにこのパターンがあれば、?falseに修正してください。
特定のエンドポイントで別のモードが必要な場合は、レコード更新を使って呼び出し箇所ごとに上書きできます。ただし、TMDb にはそのようなエンドポイントはありません。
6. 何が壊れていて、その理由
movieRecommendations は成功時に常に例外をスローします
生成された型は : async* Any で、2xx の経路では Candid の整数のみを受け付けます。
case (#Int i__) i__;
case _ throw Error.reject(… "Unexpected primitive shape");
TMDb はページネーションされたオブジェクトを返すため、成功した応答はすべて拒否されます。原因は不適切な仕様ではなく、ジェネレーターの不足です。TMDb は 200 の本文を {"type": "object", "properties": {}} — 宣言済みのプロパティがないオブジェクト — として定義しており、プラグインはこれを Any とプリミティブ専用のデコーダーとして生成するため、Candid 値をそのまま渡しません。この形になっているのは、パッケージ内でこの操作だけです。tvSeriesRecommendations は通常どおり型付けされており、動作します。
書き込み操作
本文を伴う操作は 12 個すべて、TMDb に拒否されます。これらを使った機能を構築したり、動作すると報告したりしないでください。
| アカウントへの書き込み | accountAddFavorite、accountAddToWatchlist |
| 評価 | movieAddRating、tvSeriesAddRating、tvEpisodeAddRating |
| リスト | listCreate、listAddMovie、listRemoveMovie |
| セッション | authenticationCreateSession、authenticationCreateSessionFromLogin、authenticationCreateSessionFromV4Token、authenticationDeleteSession |
最後の行に注意してください。authenticationDeleteSession は POST ではなく DELETE です — TMDb はセッション ID を本文に入れます。影響を受ける DELETE はこれだけです。ほかの 4 つはクエリパラメーターを受け取り、動作します。
原因は仕様にあり、ジェネレーターはそれを忠実に再現しています。TMDb はすべての POST 本文を、実際のスキーマではなくプレースホルダーとしてモデル化しています。
{ "type": "object", "required": ["RAW_BODY"],
"properties": { "RAW_BODY": { "type": "string", "format": "json" } } }
そのため、生成されたモデルには RAW_BODY : Text という単一のフィールドがあり、API モジュールはそれをフィールドに持つ JSON オブジェクトとしてエンコードします。実際に送信される本文は次のようになります。
{"RAW_BODY":"{\"name\":\"My list\"}"}
一方、TMDb が期待するのは {"name":"My list"} です。ペイロードは二重にラップされており、仕様に対しては正しいものの、サービスに対しては誤っています。
知っておくべき点は次のとおりです。
- プレースホルダーは共通なので、本文を伴う操作はすべて、同じ生成済みの型のいずれか(
AccountAddFavoriteRequest、ListAddMovieRequest)を受け取ります。そのため、listCreateはAccountAddFavoriteRequestを要求します。これは回避策を探すべき命名上のバグではなく、同じプレースホルダーに別々の名前が付いているだけです。 listClearは本文のない唯一の POST なので、影響を受けず動作します。- これが、セッションの行がエンドツーエンドで利用できない理由でもあります。セッションを作成できないため、ユーザーにスコープされた GET(
accountGetFavorites、accountLists、…)を呼び出すためのsessionIdがありません。これらの GET 自体は正しく生成されています。
12 個すべてを一度に修正する方法: ジェネレーターを一箇所変更し、唯一のプロパティが format: json の文字列であるリクエストスキーマを認識して、ラッパーオブジェクトとしてエンコードするのではなく、その文字列を本文としてそのまま渡します。x-body-is-text/パススルーの場合の仕組みはすでにあります。それまでは、アカウントへの書き込み操作は対象外です。
7. 応答サイズとサイクル
max_response_bytesのデフォルトはプラットフォームの最大値で、必要以上に遅く、コストも高くなります。エンドポイントが実際に返すサイズに設定してください。概要を含む 20 件の検索または discover ページでは約 300 KB、appendToResponseを指定しない単一のmovieDetailsでは約 50 KB、サブリソースを追加する場合はそれ以上です。- 複数回のアウトコールよりも
appendToResponseを優先してください。movieDetailsの"credits,images,videos"は 1 回の呼び出し、1 回分のサイクル料金で済み、4 回呼び出す必要はありません。 configurationDetailsは実質的に静的です。一度取得してvarに保持し、リクエストごとに呼び出さないでください。
8. 注意すべき点
-
空文字列とゼロは「省略」を意味します。 Text 型と Bool 型のクエリパラメーターは位置指定で、
?Tではありません。省略するには""/falseを渡してください。?Tなのは enum 型のクエリパラメーターだけです。 -
ID は
TextではなくIntです。movieId、tvSeriesId、personId、accountIdはすべて生成されたシグネチャで整数になっています。呼び出す前に型を変換してください。 -
pageは 1 始まりで、1 ページあたり 20 件です。また、page > 500 は 422 を返します。TMDb のエラーをそのまま表に出すのではなく、呼び出す前に上限を設定してください。 -
画像パスは相対パスです。
poster_path/backdrop_pathは/abc123.jpgのような形式です。先頭にhttps://image.tmdb.org/t/p/w500(またはconfigurationDetailsにある別のサイズ)を付けてください。パスだけでは何も表示されません。 -
IP あたりのレート制限は約 50 リクエスト/秒で、TMDb は
Retry-Afterを返しません。429 が返された場合は、少なくとも 1 秒待ってから再試行してください。canister 内で短い間隔で繰り返し再試行しないでください。 -
searchMultiの結果は異種です。各エントリーにはmovie、tv、personのいずれかのmedia_typeが含まれ、存在するフィールドもそれに応じて異なります(映画はtitle、テレビと人物はname)。フィールドを読み取る前にmedia_typeに応じて分岐してください。 -
TMDb の 152 個の操作のうち 28 個は除外されていますが、決めつける前に確認してください。
focusApisは、tv、movie、account、person、search、list、lists、authentication、configuration、trending、discoverのいずれかで始まるすべての operationId を保持します。そのため、あるリソースの映画またはテレビにスコープされたバリアントは含まれますが、同じリソースの単独エンドポイントは除外されます。movieWatchProviders、movieKeywords、movieReviews、movieTranslations、movieChanges、searchCompany、searchCollection、authenticationCreateGuestSessionはすべて含まれます。含まれないのはwatchProvidersMovieList、keywordDetails、reviewDetails、translations、changesMovieList、companyDetails、collectionDetails、guestSessionRatedMoviesです。仕様に照らして確認した、除外された項目の全リストは次のとおりです。alternative-names-copy certification-movie-list certifications-tv-list changes-movie-list changes-people-list changes-tv-list collection-details collection-images collection-translations company-alternative-names company-details company-images credit-details details-copy find-by-id genre-movie-list genre-tv-list guest-session-rated-movies guest-session-rated-tv guest-session-rated-tv-episodes keyword-details keyword-movies network-details review-details translations watch-provider-tv-list watch-providers-available-regions watch-providers-movie-listいずれかが必要な機能では、
focusApisにそのプレフィックスを追加し、パッケージを再生成して再公開してください。操作が見つからないと判断する前に、DefaultApi.moで camelCase 名を検索してください。
関連項目
connector-weatherapi— 同じ生成クライアントの構成を、より小規模(9 個の操作)で確認できる例です。Configパターンの別の例として役立ちます。