Show HN: Librario,一个整合了 Google 图书、ISBNDB 等数据的图书元数据 API

5作者: jamesponddotco6 个月前
<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&#x27;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&#x27;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 &#x27;Authorization: Bearer librario_ARbmrp1fjBpDywzhvrQcByA4sZ9pn7D5HEk0kmS34eqRcaujyt0enCZ&#x27; \ &#x27;https:&#x2F;&#x2F;api.librario.dev&#x2F;v1&#x2F;book&#x2F;9781328879943&#x27; | 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&#x27;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&#x2F;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&#x27;t provide much in the end.<p>The database layer is being rewritten before v1.0[4]. I&#x27;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&#x27;t my strong suit and I couldn&#x27;t confidently vouch for the code. Rather than ship something I don&#x27;t fully understand, I hired the developers from SourceHut[6] to rewrite it properly.<p>I&#x27;ve got a 5-month-old and we&#x27;re still adjusting to their schedule, so development is slow. I&#x27;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:&#x2F;&#x2F;sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;" rel="nofollow">https:&#x2F;&#x2F;sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;</a><p>[1]: <a href="https:&#x2F;&#x2F;paste.sr.ht&#x2F;~jamesponddotco&#x2F;a6c3b1130133f384cffd25b33a8ab1bc3392093c" rel="nofollow">https:&#x2F;&#x2F;paste.sr.ht&#x2F;~jamesponddotco&#x2F;a6c3b1130133f384cffd25b3...</a><p>[2]: <a href="https:&#x2F;&#x2F;todo.sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;16" rel="nofollow">https:&#x2F;&#x2F;todo.sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;16</a><p>[3]: <a href="https:&#x2F;&#x2F;todo.sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;13" rel="nofollow">https:&#x2F;&#x2F;todo.sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;13</a><p>[4]: <a href="https:&#x2F;&#x2F;todo.sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;14" rel="nofollow">https:&#x2F;&#x2F;todo.sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;14</a><p>[5]: <a href="https:&#x2F;&#x2F;sqlc.dev" rel="nofollow">https:&#x2F;&#x2F;sqlc.dev</a><p>[6]: <a href="https:&#x2F;&#x2F;sourcehut.org&#x2F;consultancy&#x2F;" rel="nofollow">https:&#x2F;&#x2F;sourcehut.org&#x2F;consultancy&#x2F;</a><p>[7]: <a href="https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=45419234">https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=45419234</a><p>[8]: <a href="https:&#x2F;&#x2F;sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;" rel="nofollow">https:&#x2F;&#x2F;sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;</a><p>[9]: <a href="https:&#x2F;&#x2F;git.sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;tree&#x2F;trunk&#x2F;item&#x2F;CONTRIBUTING.md" rel="nofollow">https:&#x2F;&#x2F;git.sr.ht&#x2F;~pagina394&#x2F;librario&#x2F;tree&#x2F;trunk&#x2F;item&#x2F;CONTRI...</a>