关于 connector-tmdb
connector-tmdb 是从 TMDb OpenAPI 规格生成的 Motoko 绑定,供在 Internet Computer canister 中读取电影和电视目录数据的开发者使用。搜索、详情、趋势和发现等读取操作可用,但文档指出多数带请求体的写入操作及电影推荐不可用。
主要功能
- 01TMDb API 的 Motoko 绑定
- 02电影、电视与人物搜索
- 03详情、演职员和媒体信息读取
- 04趋势、发现与分类目录读取
- 05使用 v4 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 规范生成。一个模块中包含 124 个操作
(Apis/DefaultApi)——规范没有标签,因此无需拆分,也没有嵌套门面。
规划应用前请先阅读:读取功能正常,写入功能不可用。 124 个操作中,111 个可用,13 个已损坏:
- 12 个带有请求正文——包括 11 个 POST 操作和
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 也会颁发 v3 API 密钥(32 位十六进制字符),通常通过 ?api_key=… 查询参数传递。此客户端无法发送该密钥。 规范只声明了一种安全方案——位于名为 Authorization 的 header 中的 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 中的 actor 将令牌作为参数传入,以保持示例自包含。在实际应用中,不要让前端持有凭据——应将其存入 actor 状态,并通过经过授权的更新调用设置:
- 仅声明字段类型。 Caffeine actor 从迁移链获取稳定状态,因此应写为
var tmdbToken : Text;——不带初始化值。在真实项目中编译时,写成var tmdbToken : Text = "";会触发M0250。 - 使用
MixinAuthorization限制设置器的访问,并且只在 actor 内构建Config时读取该字段。 - 绝不从查询方法返回该令牌,绝不记录该令牌,也绝不将其包含在返回给调用方的错误中。
connector-googlemail 是这种结构的完整示例——gmailConfig 记录由仅声明类型的 var 字段组成,设置器使用授权 mixin,迁移链的头部则提供初始值。如果应用需要由管理员管理凭据,请参照此示例;原型或调用方已受信任时,使用上面的参数形式即可。
3. 标准布局
下面的 actor 就是电影搜索功能的整个后端。它会将 TMDb 宽泛且字段几乎全为可选的响应映射为精简记录,让前端无需逐一检查 14 个字段即可使用。
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 一次调用即可返回电影、电视节目和人物 |
| 电影详情 | movieDetails、movieCredits、movieImages、movieVideos、movieSimilar、movieReleaseDates | 影片页面。不要用 movieRecommendations——见 §6 |
| 电视详情 | 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 | 仅适用于电视节目的“相似内容” |
| 发现 | discoverMovie、discoverTv | 按类型、年份、评分、语言和排序筛选浏览 |
| 账户读取 | accountGetFavorites、accountWatchlistMovies、accountWatchlistTv、accountRatedMovies、accountRatedTv、accountLists、accountDetails | 读取用户自己的列表。生成正确,但需要目前无法获取的 sessionId——见 §6。没有 accountGetWatchlist;电影和电视节目是两个独立操作 |
| 配置 | configurationDetails、configurationLanguages、configurationCountries | 图片基础 URL、语言代码。获取一次并缓存——这些内容很少变化 |
响应中的所有字段都是可选的(?T),因为 TMDb 会省略字段,而不是将其设为 null。请像 §3 中那样,在 actor 边界处理这些可选值。
排序参数是类型化变体,不是字符串——请从生成的 *SortByParameter 模块中选择(例如 #created_at_desc)。传入任意字符串将无法通过类型检查。
5. 默认不进行调用复制
defaultConfig 的 is_replicated = ?false,因此每次外部调用只由一个节点执行,响应也不会进入共识。这里的读取和写入都适合采用这种方式;这也与 0.1.1 版本有所不同,该版本默认值为 null(= 复制调用):
- 读取——目录响应稳定,因此复制调用没有收益,反而会多消耗约 13 倍的 cycles。
- 写入——
listCreate和三个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 使用的是 DELETE,而不是 POST——
TMDb 将会话 ID 放在请求体中。它是唯一受影响的 DELETE;另外四个操作使用查询参数,可以正常工作。
问题出在规范中,生成器只是如实复现了它。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. 响应大小与 cycles
max_response_bytes默认值为平台上限,这会比实际所需更慢、成本也更高。应根据端点的实际返回内容设置:带摘要的 20 条结果搜索或发现页面约需 300 KB,不使用appendToResponse的单个movieDetails约需 50 KB;追加子资源时则需要更多。- 优先使用
appendToResponse,而不是发起多次出站调用。在movieDetails中使用"credits,images,videos",只需一次调用、消耗一次 cycles,而不是四次。 configurationDetails基本不会变化——获取一次,将结果保存在var中,不要每次请求都调用。
8. 容易踩坑的地方
-
空字符串和零表示“省略”。 文本和 Bool 查询参数是位置参数,而不是
?T:传入""/false可省略其中一项。只有枚举查询参数是?T。 -
ID 是
Int,不是Text。movieId、tvSeriesId、personId、accountId在生成的签名中都是整数。调用前请先转换类型。 -
page从 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的结果包含多种类型——每个条目都带有media_type,其值为movie、tv或person,且相应字段各不相同(电影使用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中搜索其驼峰式名称。
相关项目
connector-weatherapi— 规模小得多(9 个操作)的同类生成客户端示例,可用作Config模式的另一个参考。