Skip to content

Commit 4ce1275

Browse files
authored
Merge pull request #13 from joshbeckman/feature/readwise-api-v3
new(usr): Add first pass at Readwise API V3 (support for Reader)
2 parents 2b0e51c + 9f9ce89 commit 4ce1275

File tree

11 files changed

+943
-6
lines changed

11 files changed

+943
-6
lines changed

README.md

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Gem Version](https://badge.fury.io/rb/readwise.svg)](https://badge.fury.io/rb/readwise) [![Ruby](https://github.com/joshbeckman/readwise-ruby/actions/workflows/ruby.yml/badge.svg)](https://github.com/joshbeckman/readwise-ruby/actions/workflows/ruby.yml)
44

5-
[Readwise](https://readwise.io/) is an application suite to store, revisit, and learn from your book and article highlights. This is a basic library to call the [Readwise API](https://readwise.io/api_deets) to read and write highlights.
5+
[Readwise](https://readwise.io/) is an application suite to store, revisit, and learn from your book and article highlights. This is a basic library to call the [Readwise API](https://readwise.io/api_deets) to read and write highlights, and manage Reader documents through the [Reader API](https://readwise.io/reader_api).
66

77
This library is not at 100% coverage of the API, so if you need a method that is missing, open an issue or contribute changes!
88

@@ -26,6 +26,10 @@ Or install it yourself as:
2626

2727
First, obtain an API access token from https://readwise.io/access_token.
2828

29+
### Highlights API (V2)
30+
31+
The [V2 API](https://readwise.io/api_deets) provides access to your highlights and books:
32+
2933
```ruby
3034
client = Readwise::Client.new(token: token)
3135

@@ -56,6 +60,80 @@ updated_tag = client.update_highlight_tag(highlight: highlight, tag: added_tag)
5660
client.remove_highlight_tag(highlight: highlight, tag: added_tag)
5761
```
5862

63+
### Reader API (V3)
64+
65+
The [V3 API](https://readwise.io/reader_api) provides access to Readwise Reader functionality for managing documents (articles, PDFs, etc.):
66+
67+
```ruby
68+
# Get all documents
69+
documents = client.get_documents
70+
71+
# Get documents with filters
72+
documents = client.get_documents(
73+
updated_after: '2023-01-01T00:00:00Z',
74+
location: 'new', # 'new', 'later', 'archive', or 'feed'
75+
category: 'article' # 'article', 'email', 'rss', 'highlight', 'note', 'pdf', 'epub', 'tweet', 'video'
76+
)
77+
78+
# Get a specific document
79+
document = client.get_document(document_id: '123456')
80+
81+
puts document.title
82+
puts document.author
83+
puts document.url
84+
puts document.reading_progress
85+
puts document.location
86+
puts document.category
87+
88+
# Check document properties
89+
puts document.read? # reading progress >= 85%
90+
puts document.read?(threshold: 0.5) # custom threshold
91+
puts document.parent? # is this a top-level document?
92+
puts document.child? # is this a highlight/note of another document?
93+
94+
# Check location
95+
puts document.in_new?
96+
puts document.in_later?
97+
puts document.in_archive?
98+
99+
# Check category
100+
puts document.article?
101+
puts document.pdf?
102+
puts document.epub?
103+
puts document.tweet?
104+
puts document.video?
105+
puts document.book?
106+
puts document.email?
107+
puts document.rss?
108+
puts document.highlight?
109+
puts document.note?
110+
111+
# Access timestamps
112+
puts document.created_at_time
113+
puts document.updated_at_time
114+
puts document.published_date_time
115+
116+
# Create a new document
117+
document_create = Readwise::DocumentCreate.new(
118+
url: 'https://example.com/article',
119+
title: 'My Article',
120+
author: 'John Doe',
121+
html: '<p>Article content</p>',
122+
summary: 'A brief summary',
123+
location: 'new', # 'new', 'later', 'archive', or 'feed'
124+
category: 'article', # 'article', 'email', 'rss', etc.
125+
tags: ['technology', 'programming'],
126+
notes: 'My personal notes',
127+
should_clean_html: true,
128+
saved_using: 'api'
129+
)
130+
131+
document = client.create_document(document: document_create)
132+
133+
# Create multiple documents
134+
documents = client.create_documents(documents: [document_create1, document_create2])
135+
```
136+
59137
## Development
60138

61139
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

lib/readwise.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
require 'readwise/version'
2-
require 'readwise/client'
1+
require_relative 'readwise/version'
2+
require_relative 'readwise/client'
33

44
module Readwise
55
class Error < StandardError; end

lib/readwise/client.rb

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,58 @@
33
require_relative 'book'
44
require_relative 'highlight'
55
require_relative 'tag'
6+
require_relative 'document'
67

78
module Readwise
89
class Client
910
class Error < StandardError; end
1011

1112
BASE_URL = "https://readwise.io/api/v2/"
13+
V3_BASE_URL = "https://readwise.io/api/v3/"
1214

1315
def initialize(token: nil)
1416
raise ArgumentError unless token
1517

1618
@token = token.to_s
1719
end
1820

21+
def create_document(document:)
22+
raise ArgumentError unless document.is_a?(Readwise::DocumentCreate)
23+
24+
url = V3_BASE_URL + 'save/'
25+
26+
res = post_readwise_request(url, payload: document.serialize)
27+
get_document(document_id: res['id'])
28+
end
29+
30+
def create_documents(documents: [])
31+
return [] unless documents.any?
32+
33+
documents.map do |document|
34+
create_document(document: document)
35+
end
36+
end
37+
38+
def get_document(document_id:)
39+
url = V3_BASE_URL + "list?id=#{document_id}"
40+
41+
res = get_readwise_request(url)
42+
43+
res['results'].map { |item| transform_document(item) }.first
44+
end
45+
46+
def get_documents(updated_after: nil, location: nil, category: nil)
47+
resp = documents_page(updated_after: updated_after, location: location, category: category)
48+
next_page_cursor = resp[:next_page_cursor]
49+
results = resp[:results]
50+
while next_page_cursor
51+
resp = documents_page(updated_after: updated_after, location: location, category: category, page_cursor: next_page_cursor)
52+
results.concat(resp[:results])
53+
next_page_cursor = resp[:next_page_cursor]
54+
end
55+
results.sort_by(&:created_at)
56+
end
57+
1958
def create_highlight(highlight:)
2059
create_highlights(highlights: [highlight]).first
2160
end
@@ -101,6 +140,28 @@ def export(updated_after: nil, book_ids: [])
101140

102141
private
103142

143+
def documents_page(page_cursor: nil, updated_after:, location:, category:)
144+
parsed_body = get_documents_page(page_cursor: page_cursor, updated_after: updated_after, location: location, category: category)
145+
results = parsed_body.dig('results').map do |item|
146+
transform_document(item)
147+
end
148+
{
149+
results: results,
150+
next_page_cursor: parsed_body.dig('nextPageCursor')
151+
}
152+
end
153+
154+
def get_documents_page(page_cursor: nil, updated_after:, location:, category:)
155+
params = {}
156+
params['updatedAfter'] = updated_after if updated_after
157+
params['location'] = location if location
158+
params['category'] = category if category
159+
params['pageCursor'] = page_cursor if page_cursor
160+
url = V3_BASE_URL + 'list/?' + URI.encode_www_form(params)
161+
162+
get_readwise_request(url)
163+
end
164+
104165
def export_page(page_cursor: nil, updated_after: nil, book_ids: [])
105166
parsed_body = get_export_page(page_cursor: page_cursor, updated_after: updated_after, book_ids: book_ids)
106167
results = parsed_body.dig('results').map do |item|
@@ -120,7 +181,6 @@ def get_export_page(page_cursor: nil, updated_after: nil, book_ids: [])
120181
url = BASE_URL + 'export/?' + URI.encode_www_form(params)
121182

122183
get_readwise_request(url)
123-
124184
end
125185

126186
def transform_book(res)
@@ -166,9 +226,51 @@ def transform_highlight(res)
166226
)
167227
end
168228

229+
def transform_document(res)
230+
Document.new(
231+
author: res['author'],
232+
category: res['category'],
233+
created_at: res['created_at'],
234+
html: res['html'],
235+
id: res['id'].to_s,
236+
image_url: res['image_url'],
237+
location: res['location'],
238+
notes: res['notes'],
239+
parent_id: res['parent_id'],
240+
published_date: res['published_date'],
241+
reading_progress: res['reading_progress'],
242+
site_name: res['site_name'],
243+
source: res['source'],
244+
source_url: res['source_url'],
245+
summary: res['summary'],
246+
tags: transform_tags(res['tags']),
247+
title: res['title'],
248+
updated_at: res['updated_at'],
249+
url: res['url'],
250+
word_count: res['word_count'],
251+
)
252+
end
253+
254+
def transform_tags(res)
255+
if res.is_a?(Array)
256+
res.map { |tag| transform_tag(tag) }
257+
elsif res.is_a?(Hash)
258+
res.map do |tag_id, tag|
259+
tag['id'] = tag_id
260+
transform_tag(tag)
261+
end
262+
else
263+
[]
264+
end
265+
end
266+
169267
def transform_tag(res)
268+
if res.is_a?(String)
269+
return Tag.new(name: res)
270+
end
271+
170272
Tag.new(
171-
tag_id: res['id'].to_s,
273+
tag_id: res['id']&.to_s,
172274
name: res['name'],
173275
)
174276
end
@@ -181,7 +283,7 @@ def get_readwise_request(url)
181283
http.request(req)
182284
end
183285

184-
raise Error, 'Get request failed' unless res.is_a?(Net::HTTPSuccess)
286+
raise Error, "Get request failed with status code: #{res.code}" unless res.is_a?(Net::HTTPSuccess)
185287

186288
JSON.parse(res.body)
187289
end

0 commit comments

Comments
 (0)