Tmdb:The Movie Database 的缓存包装器






4.14/5 (5投票s)
使用此包装器轻松高效地查询 api.themoviedb.org/3/
引言
在试验我的 JSON 库并围绕它们构建一些新代码时,我决定通过将它们插入 themoviedb.org 的内容并使用提供的 JSON/REST 接口访问它们的 API 来创建一个真实世界的用例。它最初是一个演示应用程序和用例。
该用例扩展到 30 多个包装器类,涵盖了 API 的几乎所有方面,以及一个几乎自动的、用于 JSON 支持对象的内存中缓存系统。
免责声明:这个项目最初是一个演示。我没有为它编写测试。幸运的是,它很容易维护,特别是考虑到最可能出现的错误。使用风险自负。但我会维护它,如果您有错误要报告,我会立即处理。
背景
themoviedb.org 编目电视节目、电影和演员,以便您可以检索所有类型的信息。它公开了一个 JSON/REST API,您可以使用它来查询内容。这些数据对于各种事情都很有用,从在 Plex 等程序中呈现友好的节目信息,到组织和编目您自己的数字节目和电影收藏(如果您是像我一样翻录内容的类型——我太懒了,不想更换光盘!)
与此同时,JSON 是一种简单的交换格式,它几乎取代了 XML,成为网络上流行的事实数据传输交换格式。它比 XML 更轻量,解析起来非常容易,而且非常直观。它尽可能地简单,这可能是一种福音,但有时也是一种诅咒(例如,没有内置的模式信息)
这个项目涵盖了两者,但我们将首先快速介绍如何使用随附的 JSON API。
使用 JSON API
这只是一个概述。有关完整介绍,包括缓存 rpc 机制,请参阅我的这篇文章。
这个 TMDb 库实际上只是那个 JSON 库的演示项目,但我想要一些真实世界的东西来冲击它。
概念化这个混乱的局面
大多数时候,JSON 不以文本形式表示,而是以嵌套对象图的形式表示。它的工作原理是每个 JSON 元素映射到某个 .NET 类型;JSON object {}
映射到 IDictionary<string,object>
,JSON array []
映射到 IList<object>
,JSON string
和 boolean
类型映射到它们各自的 .NET 类型,null 映射到 System.Object
,JSON numeric
数字类型根据其容纳能力映射到 System.Int32
、System.Int64
、System.Numerics.BigInteger
或 System.Double
——首选是第一个。
以这种方式映射对象会从 JSON 创建一个对象树。如果我们将这些树互连,并回收相同的分支,我们就创建了一个网格或一个图,因为一个节点可以有多个父节点。这就是为什么我们称它为对象图
而不是树。如果我们将图序列化,它将表示为一棵树,其中“回收”的分支被复制到所有引用它们的位置。如果您创建一个父节点,并且该父节点被其后代之一在这些图中引用(一个循环),然后尝试序列化它,则行为是未定义的,通常非常非常糟糕。
编写这个混乱的程序
JsonObject
不必使用,但它薄薄地封装了一个字典,并提供了动态/DLR 调用下沉,其中键是属性名称,并且它提供了一些用于读取、写入和操作 JSON 的方法,例如 CopyTo()
、Get()
、CreateSubtree()
和 Select()
——后者接受一个 jsonpath 表达式并用它查询图。再一次,如果您尝试查询一个递归图,结果是未定义的,并且通常很糟糕。一般规则是,不要尝试使您的 JSON 递归。仅仅因为您可以做某事并不意味着您应该这样做。相等性使用值语义完成。也就是说,如果两个对象包含相同的数据,则它们被认为是相等的。这允许将整个 JSON 树用作字典键,但请小心使用。该功能很有用,但一点也不快。
JsonArray
类似地封装了一个列表并提供值语义,以及一个 ToArray()
方法,可以帮助您将数组转换为更可用的类型。其中一个重载接受一个 lambda,允许您从源 JSON 元素创建目标数组元素。
// merge src into dst
var src = JsonObject.Parse("{\"id\":2,\"foo\":{\"foobar\":\"baz\"}");
Console.WriteLine(src);
var dst = JsonObject.Parse("{\"id\":1,\"foo\":{\"id\":3} }");
Console.WriteLine(dst);
JsonObject.CopyTo(src, dst);
Console.WriteLine(dst);
CreateSubtree()
接受一个 JSON 对象和一系列路径段,并在 JSON 中创建该树。无论哪种方式,都会遵循路径并返回最终元素。这可以用于快速轻松地创建或遍历 JSON 数据中的路径。如果您尝试替换已存在但不是 object
的内容,例如当您尝试在现有 array
的路径中创建时,它会抛出异常。
// create /foobar/baz under obj
var obj = JsonObject.Parse("{\"foo\":\"bar\"}");
var obj2 = JsonObject.CreateSubtree(obj, "foobar", "baz");
obj2.Add("result", 1);
Console.WriteLine(obj);
然后是 Parse()
、LoadFrom()
、ReadFrom()
和 LoadFromUrl()
,每个都接受某种形式的输入(例如一些文本或一个文件)并从中返回一个 JSON 对象树。这些分别补充了 ToString()
、SaveTo()
和 WriteTo()
,尽管没有直接的方法可以保存到 URL!请参阅上面 Parse()
的代码,但所有加载和读取方法的工作方式都相同,写入和保存方法则以相反的方式工作。简单。
或者,您可以使用 JsonTextReader
流式传输 JSON,而无需在内存中创建整个文档的模型——您可以选择性地这样做。这与 XmlReader/XmlTextReader
的工作方式非常相似。但是,它还包括 ParseSubtree()
,它将当前子树作为 JSON object
返回;SkipSubtree()
,它快速跳过子树;以及 SkipToField()
,它前进到指定的字段。
该库还包含一个简单的基于 JSON/REST 的 RPC 方法,带有 Tmdb 项目广泛使用的缓存。
JsonPath 支持代码由 Atif Aziz 根据 MIT 许可证提供。版权所有 (c) 2007 (源代码中完全受版权保护)
使用 Tmdb API
根 API 的概念化
此 API 松散地包装了此处描述的 The Movie Database API。大多数名称都保持不变,但已重命名为 .NET 约定。例如,JSON 字段 "imdb_id"
变为 ImdbId
。
这个烂摊子的根源是 Tmdb
,它是一个静态类,公开了访问 TMDb API 终结点的方法,例如 SearchMovies()
和 ApiKey
——在 themoviedb.org 创建账户后在此处创建您的 API 密钥。另一个属性是 Language
,允许您将语言设置为您想要的 ISO 语言,例如“en-US”表示英语。一旦您开始使用,就不应设置前两个属性,因此请先设置它们。访问 Json
属性可让您访问 TMDb 实体的线程本地缓存(请参阅有关缓存的部分)。
除了 API 访问函数,还有几个缓存函数(稍后介绍)和一些辅助函数,包括 GetImageUrl()
,它允许您将从 TMDb 获取的图像“路径”**映射到真实的 URL。
** 实际上不是路径。它只是路径的一部分,并且从 TMDb 返回时根目录无效,因此它本身无法使用。
双层缓存的概念化
出于性能原因,存在两级缓存。
主缓存:主缓存位于内存中,按线程分配,并且非常激进。它从不使其缓存过期。它更新的唯一时间是当您执行某些操作,否则会导致它从服务器刷新某些内容时。它旨在用于批处理,然后通过使用 Tmdb.ClearCache()
手动使缓存过期来丢弃。从服务器返回的一些数据从未缓存,例如会话或搜索查询(但是,在后一种情况下,单个结果被缓存,但搜索本身没有)。通常,缓存的项目通过属性访问,未缓存的项目通过方法访问。还可以加载、保存和合并整个缓存,以便使其分布式(例如用于服务器场)。
二级缓存:二级缓存是基于 URL 的,其工作原理基本上与您的网络浏览器缓存类似。它会定期与服务器重新检查,尽管您可以更改检查的频率。当此处的一个项目被认为是未缓存时,它仍然可以在此级别进行缓存。这意味着本文和源代码注释中使用的“未缓存”仅指主缓存。二级缓存是应用程序全局的,并由操作系统和运行时环境自动管理。您可以通过 Tmdb.CacheLevel
属性进行设置。
Tmdb 实体概念化
TMDb API 中的每个重要数据项都派生自 TmdbEntity
。此类别只表示库中所有实体对象的基类。每个实体都包装了 TMDb 的特定数据集。例如,有 TmdbShow
表示电视节目,TmdbMovie
表示电影。实体本身并没有太多作用。它主要只是一个约定,规定它接受带规范化 JSON 数据的构造函数,并通过 Json
属性公开其数据。这意味着它将其所有状态都基于该 Json
对象。这很重要。
一些实体可以从远程端点和缓存中检索自身。这些实体具有一个独特的身份,称为 PathIdentity
,表示为一系列路径段。这是对象在远程存储库和本地缓存中的位置。它们(大部分)共享数据布局,因此保持简单。这些对象具有用于管理和从缓存中检索项目的受保护方法。请参阅 TmdbCachedEntity
的代码。
此外,一些实体可能还支持数字或字符串标识符 (ID)。当缓存的实体支持时,整数 ID 由 TmdbCachedEntityWithId
表示,字符串 ID 由 TmdbCachedEntityWithId2
表示。这些实体只提供一个接受 ID 的重载构造函数,并通过 Id
属性公开该 ID。
通常,实体支持将其自身植根于 Tmdb.Json
对象缓存中,并从远程 TMDb API 端点获取自身。实体只是包装其底层的 Json。这些包装器可以重复使用和丢弃。重要的是底层 JSON 会一直存在。每个实体都有一个关联的对象图,表示其 JSON,由 Json
属性公开。对象上的其他属性只是包装此数据。如果请求的数据未在其 Json
图中找到,它会转到远程服务器获取所需的数据,然后将其存储在 Json
图中。每个 Json
图都根植于 Tmdb.Json
之下,Tmdb.Json
再次作为整个图的根。
有些实体无法从服务器获取自身。也许它们只是代表另一个实体的一些子数据,并且它们没有自己的 TMDb ID。在这种情况下,对象作为其父实体缓存的一部分进行缓存,并且实体只能通过表示其所有属性的完整 JSON 图来创建。这是因为显然,它无法根据需要从服务器获取数据,因为它无法识别自身,因此其所有数据必须在构建时存在。这是必需的,因为许多 TMDb API 返回的查询具有多级嵌套数据,并且所有这些数据都必须表示。有些实体从未被缓存。像 TmdbSession
这样的实体缓存起来根本没有意义。
TmdbEntity 派生的概念化
如果我们创建一个新的 TmdbShow
对象(TMDb 电视节目的包装器),我们可以将一个 id
传递给构造函数。这将使 JSON 对象最初只有 id
。其他 JSON 属性,如 "name"
和 "first_air_date"
将在您分别查询节目的 Name
和 FirstAirDate
时按需获取。请注意,通常,获取一个属性会检索实体的大部分属性,因为 TMDb 为每个查询返回大量数据。我们使用所有这些数据,将其放入缓存中,以避免不必要地访问远程服务器。
using TmdbApi;
...
Tmdb.ApiKey = "myApiKey"; // change this to something valid
...
var show = new TmdbShow(2919); // This is the TV show "Burn Notice"
// doing this creates no network traffic yet
Console.WriteLine(show.Json); // currently only writes {"id":2919}
// the following will cause an http request
Console.WriteLine(show.Name); // writes "Burn Notice" to the console.
// the following will *not* cause a request because the last one
// already fetched this data.
Console.WriteLine(show.Overview); // writes the burn notice overview
Console.WriteLine(show.Json); // writes a lot of json
...
var show2 = new TmdbShow(2919);
// none of the following causes network traffic
Console.WriteLine(show==show2); // writes "true" since they have the same id.
Console.WriteLine(show2.TotalSeasons); // writes 7
Console.WriteLine(show2.Json); // same as writing show.Json - both point to the same place.
...
分页函数的概念化
没有人想按 ID 获取电影和节目。您通常会使用 SearchShows()
/DiscoverShows()
来查找电视节目(以及 SearchMovies()
/DiscoverMovies()
来查找电影)。这些就是所谓的“分页函数”。
// fetch the top result (no error checking)
var show = Tmdb.SearchShows("Star Trek", minPage: 0, maxPage: 0)[0];
分页函数一次返回查询的一部分或多页。这些函数接受 minPage
和 maxPage
。如果未指定,则返回所有结果。服务器上的页码从 1 开始,但在本库中从零开始,因此第一页是 0,而不是 1。每页都需要自己的 HTTP 请求,因此返回多页会产生大量流量。
分页函数不缓存。也就是说,这些将来可能会被缓存,但目前不会。然而,它们返回的每个结果都会逐项缓存,但搜索请求本身不会存储,因此如果再次运行,搜索将再次运行。例如,如果您使用“Hunting”调用 SearchMovies()
,返回的每部电影(如“Good Will Hunting”)都将存储在缓存中,但再次运行搜索仍将导致至少一个 HTTP 请求。因此,请尽可能减少这些函数的使用。
有关各个字段的文档,请参阅 TMDb API 文档此处。
历史
- 2019年9月5日 - 初次提交