Show HN: Librario,一个整合了 Google 图书、ISBNDB 等数据的图书元数据 API
5 分•作者: jamesponddotco•6 个月前
<i>TLDR:</i> Librario 是一个图书元数据 API,它从 Google Books、ISBNDB 和 Hardcover 聚合数据到一个响应中,解决了没有单一来源拥有完整图书信息的问题。它目前处于 pre-alpha 阶段,采用 AGPL 许可证,现在就可以试用[0]。
我和我的妻子有一个大约有 1800 本书的个人图书馆。我开始为我们制作一个图书馆管理工具,但我很快意识到我需要一个图书信息的数据来源,而现有的解决方案都无法提供我需要的所有数据。一个可能提供系列信息,另一个可能提供体裁信息,还有一个可能提供好的封面,但没有一个能提供所有信息。
所以我开始着手开发 Librario,这是一个用 Go 编写的图书元数据聚合 API。它从多个来源(Google Books、ISBNDB、Hardcover,接下来将支持 Goodreads 和 Anna's Archive)获取图书信息,合并所有信息,并将其保存到 PostgreSQL 数据库中以供将来查找。其理念是,随着查询的图书越来越多,数据库会变得越来越强大。
你可以在这里查看一个示例响应[1],或者自己尝试一下:
```
curl -s -H 'Authorization: Bearer librario_ARbmrp1fjBpDywzhvrQcByA4sZ9pn7D5HEk0kmS34eqRcaujyt0enCZ' \
'https://api.librario.dev/v1/book/9781328879943' | jq .
```
这还是 pre-alpha 阶段,运行在一个小型 VPS 上,请注意这一点。我从未达到第三方服务的限制,所以根据这篇文章的反响,我将(或不会)发现代码是否能很好地处理这个问题。
合并是该服务的核心,弄清楚如何组合来自不同来源的相互冲突的数据是最难的部分。最后,我决定使用特定于字段的策略,这些策略相当简单,但目前有效。
每个提取器都有一个优先级,结果在合并之前按该优先级排序。但仅有优先级是不够的,因此不同的字段需要不同的处理方式。
例如:
- 标题使用评分系统。我惩罚包含括号的标题,因为来源有时会将副标题塞入主标题字段。过长的标题(80 个字符以上)也会受到惩罚,因为它们通常包含属于其他地方的版本信息或其他元数据。
- 封面收集所有候选 URL,然后一个单独的获取器根据尺寸和质量下载并对其进行评分。最好的一个会被存储在本地并从服务器提供。
对于大多数其他字段(出版商、语言、页数),我只按优先级取第一个非空值。简单,但有效。
最近添加了一个缓存层[2],这大大加快了速度。我曾考虑在某个时候从 <i>net/http</i> 迁移到 <i>fiber</i>[3],但最终放弃了。使用标准库之外的东西感觉不对,而且这种迁移最终也没有提供太多好处。
数据库层正在 v1.0 之前被重写[4]。说实话:最初的模式是由 AI 编写的,虽然我试图用 SQLC[5] 和良好的文档来引导它走向正确的方向,但数据库设计不是我的强项,我无法自信地保证代码的质量。与其发布一些我没有完全理解的东西,我聘请了 SourceHut[6] 的开发人员来正确地重写它。
我有一个 5 个月大的孩子,我们还在适应他们的作息时间,所以开发进度很慢。我之前在一些 HN 帖子中提到过这个项目[7],所以很高兴终于能让人们试用一下。
代码采用 AGPL 许可证,托管在 SourceHut 上[8]。
非常欢迎反馈和补丁[9] :)
[0]: <a href="https://sr.ht/~pagina394/librario/" rel="nofollow">https://sr.ht/~pagina394/librario/</a>
[1]: <a href="https://paste.sr.ht/~jamesponddotco/a6c3b1130133f384cffd25b33a8ab1bc3392093c" rel="nofollow">https://paste.sr.ht/~jamesponddotco/a6c3b1130133f3...</a>
[2]: <a href="https://todo.sr.ht/~pagina394/librario/16" rel="nofollow">https://todo.sr.ht/~pagina394/librario/16</a>
[3]: <a href="https://todo.sr.ht/~pagina394/librario/13" rel="nofollow">https://todo.sr.ht/~pagina394/librario/13</a>
[4]: <a href="https://todo.sr.ht/~pagina394/librario/14" rel="nofollow">https://todo.sr.ht/~pagina394/librario/14</a>
[5]: <a href="https://sqlc.dev" rel="nofollow">https://sqlc.dev</a>
[6]: <a href="https://sourcehut.org/consultancy/" rel="nofollow">https://sourcehut.org/consultancy/</a>
[7]: <a href="https://news.ycombinator.com/item?id=45419234">https://news.ycombinator.com/item?id=45419234</a>
[8]: <a href="https://sr.ht/~pagina394/librario/" rel="nofollow">https://sr.ht/~pagina394/librario/</a>
[9]: <a href="https://git.sr.ht/~pagina394/librario/tree/trunk/item/CONTRIBUTING.md" rel="nofollow">https://git.sr.ht/~pagina394/librario/tree/trunk/item/CONTRI...</a>
查看原文
<i>TLDR:</i> Librario is a book metadata API that aggregates data from Google Books, ISBNDB, and Hardcover into a single response, solving the problem of no single source having complete book information. It's currently pre-alpha, AGPL-licensed, and available to try now[0].<p>My wife and I have a personal library with around 1,800 books. I started working on a library management tool for us, but I quickly realized I needed a source of data for book information, and none of the solutions available provided all the data I needed. One might provide the series, the other might provide genres, and another might provide a good cover, but none provided everything.<p>So I started working on Librario, a book metadata aggregation API written in Go. It fetches information about books from multiple sources (Google Books, ISBNDB, Hardcover. Working on Goodreads and Anna's Archive next.), merges everything, and saves it all to a PostgreSQL database for future lookups. The idea is that the database gets stronger over time as more books are queried.<p>You can see an example response here[1], or try it yourself:<p><pre><code> curl -s -H 'Authorization: Bearer librario_ARbmrp1fjBpDywzhvrQcByA4sZ9pn7D5HEk0kmS34eqRcaujyt0enCZ' \
'https://api.librario.dev/v1/book/9781328879943' | jq .
</code></pre>
This is pre-alpha and runs on a small VPS, so keep that in mind. I never hit the limits in the third-party services, so depending on how this post goes, I’ll or will not find out if the code handles that well.<p>The merger is the heart of the service, and figuring out how to combine conflicting data from different sources was the hardest part. In the end I decided to use field-specific strategies which are quite naive, but work for now.<p>Each extractor has a priority, and results are sorted by that priority before merging. But priority alone isn't enough, so different fields need different treatment.<p>For example:<p>- Titles use a scoring system. I penalize titles containing parentheses or brackets because sources sometimes shove subtitles into the main title field. Overly long titles (80+ chars) also get penalized since they often contain edition information or other metadata that belongs elsewhere.<p>- Covers collect all candidate URLs, then a separate fetcher downloads and scores them by dimensions and quality. The best one gets stored locally and served from the server.<p>For most other fields (publisher, language, page count), I just take the first non-empty value by priority. Simple, but it works.<p>Recently added a caching layer[2] which sped things up nicely. I considered migrating from <i>net/http</i> to <i>fiber</i> at some point[3], but decided against it. Going outside the standard library felt wrong, and the migration didn't provide much in the end.<p>The database layer is being rewritten before v1.0[4]. I'll be honest: the original schema was written by AI, and while I tried to guide it in the right direction with SQLC[5] and good documentation, database design isn't my strong suit and I couldn't confidently vouch for the code. Rather than ship something I don't fully understand, I hired the developers from SourceHut[6] to rewrite it properly.<p>I've got a 5-month-old and we're still adjusting to their schedule, so development is slow. I've mentioned this project in a few HN threads before[7], so I’m pretty happy to finally have something people can try.<p>Code is AGPL and on SourceHut[8].<p>Feedback and patches[9] are very welcome :)<p>[0]: <a href="https://sr.ht/~pagina394/librario/" rel="nofollow">https://sr.ht/~pagina394/librario/</a><p>[1]: <a href="https://paste.sr.ht/~jamesponddotco/a6c3b1130133f384cffd25b33a8ab1bc3392093c" rel="nofollow">https://paste.sr.ht/~jamesponddotco/a6c3b1130133f384cffd25b3...</a><p>[2]: <a href="https://todo.sr.ht/~pagina394/librario/16" rel="nofollow">https://todo.sr.ht/~pagina394/librario/16</a><p>[3]: <a href="https://todo.sr.ht/~pagina394/librario/13" rel="nofollow">https://todo.sr.ht/~pagina394/librario/13</a><p>[4]: <a href="https://todo.sr.ht/~pagina394/librario/14" rel="nofollow">https://todo.sr.ht/~pagina394/librario/14</a><p>[5]: <a href="https://sqlc.dev" rel="nofollow">https://sqlc.dev</a><p>[6]: <a href="https://sourcehut.org/consultancy/" rel="nofollow">https://sourcehut.org/consultancy/</a><p>[7]: <a href="https://news.ycombinator.com/item?id=45419234">https://news.ycombinator.com/item?id=45419234</a><p>[8]: <a href="https://sr.ht/~pagina394/librario/" rel="nofollow">https://sr.ht/~pagina394/librario/</a><p>[9]: <a href="https://git.sr.ht/~pagina394/librario/tree/trunk/item/CONTRIBUTING.md" rel="nofollow">https://git.sr.ht/~pagina394/librario/tree/trunk/item/CONTRI...</a>