Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(usr): Add first pass at Readwise API V3 (support for Reader) #13

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/readwise.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require 'readwise/version'
require 'readwise/client'
require_relative 'readwise/version'
require_relative 'readwise/client'

module Readwise
class Error < StandardError; end
Expand Down
87 changes: 86 additions & 1 deletion lib/readwise/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,58 @@
require_relative 'book'
require_relative 'highlight'
require_relative 'tag'
require_relative 'document'
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

tODO: update the README with examples of these methods and a disclaimer that they are in beta and liable to change.


module Readwise
class Client
class Error < StandardError; end

BASE_URL = "https://readwise.io/api/v2/"
V3_BASE_URL = "https://readwise.io/api/v3/"

def initialize(token: nil)
raise ArgumentError unless token

@token = token.to_s
end

def create_document(document:)
raise ArgumentError unless document.is_a?(Readwise::DocumentCreate)

url = V3_BASE_URL + 'save/'

res = post_readwise_request(url, payload: document.serialize)
get_document(document_id: res['id'])
end

def create_documents(documents: [])
return [] unless documents.any?

documents.map do |document|
create_document(document: document)
end
end

def get_document(document_id:)
url = V3_BASE_URL + "list?id=#{document_id}"

res = get_readwise_request(url)

res['results'].map { |item| transform_document(item) }.first
end

def get_documents(updated_after: nil, location: nil, category: nil)
resp = documents_page(updated_after: updated_after, location: location, category: category)
next_page_cursor = resp[:next_page_cursor]
results = resp[:results]
while next_page_cursor
resp = documents_page(updated_after: updated_after, location: location, category: category, page_cursor: next_page_cursor)
results.concat(resp[:results])
next_page_cursor = resp[:next_page_cursor]
end
results.sort_by(&:created_at)
end

def create_highlight(highlight:)
create_highlights(highlights: [highlight]).first
end
Expand Down Expand Up @@ -101,6 +140,28 @@ def export(updated_after: nil, book_ids: [])

private

def documents_page(page_cursor: nil, updated_after:, location:, category:)
parsed_body = get_documents_page(page_cursor: page_cursor, updated_after: updated_after, location: location, category: category)
results = parsed_body.dig('results').map do |item|
transform_document(item)
end
{
results: results,
next_page_cursor: parsed_body.dig('nextPageCursor')
}
end

def get_documents_page(page_cursor: nil, updated_after:, location:, category:)
params = {}
params['updatedAfter'] = updated_after if updated_after
params['location'] = location if location
params['category'] = category if category
params['pageCursor'] = page_cursor if page_cursor
url = V3_BASE_URL + 'list/?' + URI.encode_www_form(params)

get_readwise_request(url)
end

def export_page(page_cursor: nil, updated_after: nil, book_ids: [])
parsed_body = get_export_page(page_cursor: page_cursor, updated_after: updated_after, book_ids: book_ids)
results = parsed_body.dig('results').map do |item|
Expand All @@ -120,7 +181,6 @@ def get_export_page(page_cursor: nil, updated_after: nil, book_ids: [])
url = BASE_URL + 'export/?' + URI.encode_www_form(params)

get_readwise_request(url)

end

def transform_book(res)
Expand Down Expand Up @@ -166,6 +226,31 @@ def transform_highlight(res)
)
end

def transform_document(res)
Document.new(
author: res['author'],
category: res['category'],
created_at: res['created_at'],
html: res['html'],
id: res['id'].to_s,
image_url: res['image_url'],
location: res['location'],
notes: res['notes'],
parent_id: res['parent_id'],
published_date: res['published_date'],
reading_progress: res['reading_progress'],
site_name: res['site_name'],
source: res['source'],
source_url: res['source_url'],
summary: res['summary'],
tags: (res['tags'] || []).map { |tag| transform_tag(tag) },
title: res['title'],
updated_at: res['updated_at'],
url: res['url'],
word_count: res['word_count'],
)
end

def transform_tag(res)
Tag.new(
tag_id: res['id'].to_s,
Expand Down
140 changes: 140 additions & 0 deletions lib/readwise/document.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
require 'time'

module Readwise
Document = Struct.new(
'ReadwiseDocument',
:author,
:category, # One of: article, email, rss, highlight, note, pdf, epub, tweet or video.
# Default is guessed based on the URL, usually article.
:created_at,
:html,
:id,
:image_url,
:location, # One of: new, later, archive or feed. Default is new.
:notes,
:published_date,
:reading_progress,
:site_name,
:source,
:source_url,
:summary,
:tags,
:title,
:updated_at,
:url,
:word_count,
:parent_id, # both highlights and notes made in Reader are also considered Documents.
# Highlights and notes will have `parent_id` set, which is the Document id
# of the article/book/etc and highlight that they belong to, respectively.
keyword_init: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw if you can choose to only support Ruby 3.2 or newer then it defaults to keyword_init.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷🏼 It's not that burdensome to keep around. Ruby 3.0 isn't EOL until next year, I think.

) do
def created_at_time
return unless created_at

Time.parse(created_at)
end

def updated_at_time
return unless updated_at

Time.parse(updated_at)
end

def published_date_time
return unless published_date

Time.at(published_date/1000)
end

def read?(threshold: 0.85)
reading_progress >= threshold
end

def parent?
parent_id.nil?
end

def child?
!parent?
end

def in_new?
location == 'new'
end

def in_later?
location == 'later'
end

def in_archive?
location == 'archive'
end

def pdf?
category == 'pdf'
end

def epub?
category == 'epub'
end

def tweet?
category == 'tweet'
end

def video?
category == 'video'
end

def article?
category == 'article'
end

def book?
category == 'book'
end

def email?
category == 'email'
end

def rss?
category == 'rss'
end

def highlight?
category == 'highlight'
end

def note?
category == 'note'
end

def serialize
to_h
end
end

DocumentCreate = Struct.new(
'ReadwiseDocumentCreate',
:author,
:category, # One of: article, email, rss, highlight, note, pdf, epub, tweet or video.
# Default is guessed based on the URL, usually article.
:html,
:image_url,
:location, # One of: new, later, archive or feed. Default is new.
:notes,
:published_date,
:saved_using,
:should_clean_html,
:summary,
:tags,
:title,
:url,
keyword_init: true
) do
def serialize
to_h.compact
end
end
end
2 changes: 2 additions & 0 deletions spec/readwise_spec.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'readwise'
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

TODO: Write tests for the new methods


RSpec.describe Readwise do
it "has a version number" do
expect(Readwise::VERSION).not_to be nil
Expand Down
Loading