Skip to content

Commit

Permalink
Readwise integration
Browse files Browse the repository at this point in the history
- boilerplate for communitation
- WIP: fetch all
- WIP: parse response - stacked on nanoseconds issue
- Added api models
  • Loading branch information
michmazur committed Mar 29, 2024
1 parent ebb5698 commit 36eb3a1
Show file tree
Hide file tree
Showing 12 changed files with 487 additions and 8 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.env
*.env

# Compiled class file
*.class

Expand Down Expand Up @@ -171,3 +174,4 @@ src/main/java/eu/cybershu/pocketstats/pocket/api/.DS_Store
src/.DS_Store
src/main/.DS_Store
src/main/raporting/.DS_Store
!/.env
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PocketStats

App for visualising [GetPocket](https://getpocket.com/) usage stats.
App for visualising [GetPocket](https://getpocket.com/) and [Reader](https://read.readwise.io/) usage stats.

## Config

Expand All @@ -9,6 +9,7 @@ App for visualising [GetPocket](https://getpocket.com/) usage stats.
#### Environment variables
- `BACKEND_URL` - backend url, default: http://localhost:8080
- `POCKET_CONSUMER_KEY` - get pocket consumer key
- `READER_ACCESS_KEY` - readwise access key
- `MONGODB_HOST` - mongo db host
- `MONGODB_URI` - full mongodb uri with login and pass

Expand All @@ -25,8 +26,6 @@ example env variables for local instance workig with dockerized MongoDB
#### Database
Docker service file with mongodb can be find here: [mongodb.yml](https://github.com/michmzr/PocketStats/blob/master/src/main/docker/mongodb.yml)



### Frontend
#### Environment variables
- `VUE_APP_BACKEND_URL` - java backend url
Expand Down
11 changes: 6 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ ext {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'commons-validator:commons-validator:1.7'
implementation 'commons-validator:commons-validator:1.8.0'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.14.2'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.17.0'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.17.0'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'
implementation 'com.google.code.findbugs:jsr305:3.0.2'
Expand Down Expand Up @@ -125,4 +126,4 @@ task copyFrontendToBuild(type: Copy) {
dependsOn 'vueBuild'
from "$projectDir/src/frontend/dist/"
into "$buildDir/resources/main/static"
}
}
37 changes: 37 additions & 0 deletions src/main/java/eu/cybershu/pocketstats/reader/api/Category.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package eu.cybershu.pocketstats.reader.api;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

public enum Category {
ARTICLE("article"),
EMAIL("email"),
RSS("rss"),
HIGHLIGHT("highlight"),
NOTE("note"),
PDF("pdf"),
EPUB("epub"),
TWEET("tweet"),
VIDEO("video");

private final String value;

Category(String value) {
this.value = value;
}

@JsonValue
public String getValue() {
return value;
}

@JsonCreator
public static Category forValue(String value) {
for (Category category : Category.values()) {
if (category.getValue().equals(value)) {
return category;
}
}
throw new IllegalArgumentException("Unknown category: " + value);
}
}
34 changes: 34 additions & 0 deletions src/main/java/eu/cybershu/pocketstats/reader/api/Location.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package eu.cybershu.pocketstats.reader.api;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

//values: new, later, shortlist, archive, feed
public enum Location {
NEW("new"),
LATER("later"),
SHORTLIST("shortlist"),
ARCHIVE("archive"),
FEED("feed");

private final String value;

Location(String value) {
this.value = value;
}

@JsonValue
public String getValue() {
return value;
}

@JsonCreator
public static Location forValue(String value) {
for (Location location : Location.values()) {
if (location.getValue().equals(value)) {
return location;
}
}
throw new IllegalArgumentException("Unknown location: " + value);
}
}
127 changes: 127 additions & 0 deletions src/main/java/eu/cybershu/pocketstats/reader/api/ReaderApiService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package eu.cybershu.pocketstats.reader.api;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import eu.cybershu.pocketstats.pocket.api.ApiXHeaders;
import eu.cybershu.pocketstats.utils.RequestUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
* Service for interacting with the Readwise Reader API: https://readwise.io/reader_api
*/
@Slf4j
@Service
public class ReaderApiService {
private final String readewiseReaderListUrl = "https://readwise.io/api/v3/list/?";

private final HttpClient client;

private final ObjectMapper mapper;

public ReaderApiService() {
this.client = HttpClient
.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(60))
.build();
this.mapper = new ObjectMapper();
this.mapper
.registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}

/**
* Key Type Description Required
* id string The document's unique id. Using this parameter it will return just one document, if found. no
* updatedAfter string (formatted as ISO 8601 date) Fetch only documents updated after this date no
* location string The document's location, could be one of: new, later, shortlist, archive, feed no
* category string The document's category, could be one of: article, email, rss, highlight, note, pdf, epub, tweet, video no
* pageCursor string A string returned by a previous request to this endpoint. Use it to get the next page of documents if there are too many for one request. no
*/
public List<ReaderItem> fetchList(String accessToken, ReadwiseFetchParams params) throws IOException, InterruptedException {
log.info("Fetching Readwise list with params: {}", params);

List<ReaderItem> items = new LinkedList<>();
ReadwiseFetchPaginationParams pageParams = ReadwiseFetchPaginationParams
.builder()
.category(params.category())
.location(params.location())
.pageCursor(null)
.build();

do {
ReaderListResponse response = fetchPage(accessToken, pageParams);
items.addAll(response.results());

pageParams = ReadwiseFetchPaginationParams
.builder()
.category(params.category())
.location(params.location())
.pageCursor(response.nextPageCursor())
.build();
} while (pageParams.pageCursor() != null);

return items;
}

private ReaderListResponse fetchPage(String accessToken, ReadwiseFetchPaginationParams params) throws IOException, InterruptedException {
log.info("Fetching Readwise list with params: {}", params);

String url = readewiseReaderListUrl + params.toQueryParams();
log.debug("url: {}", url);

HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create(url))
.header("Authorization", "Token " + accessToken)
.build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

logResponse(response);

switch (response.statusCode()) {
case 200:
String body = response.body();

return mapper.readValue(body, ReaderListResponse.class);
case 401:
break;
case 400:
case 403:
case 500:
case 504:
default:
throw new IllegalStateException("Not expected http response code " + response.statusCode());
}

throw new IllegalStateException("Not expected http response code " + response.statusCode());
}

private void logResponse(HttpResponse<String> response) {
log.debug("status: {}", response.statusCode());

// print response body
if (response.statusCode() != HttpStatus.OK.value()) {
log.debug("response: {}", response.body());
}
}
}
56 changes: 56 additions & 0 deletions src/main/java/eu/cybershu/pocketstats/reader/api/ReaderItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package eu.cybershu.pocketstats.reader.api;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.Instant;
import java.util.List;

/**
{
* "id": "01gkqt8nbms4t698abcdvcswvf",
* "url": "https://readwise.io/new/read/01gkqt8nbms4t698abcdvcswvf",
* "source_url": "https://www.vanityfair.com/news/2022/10/covid-origins-investigation-wuhan-lab",
* "title": "COVID-19 Origins: Investigating a “Complex and Grave Situation” Inside a Wuhan Lab",
* "author": "Condé Nast",
* "source": "Reader add from import URL",
* "category": "article",
* "location": "new",
* "tags": {},
* "site_name": "Vanity Fair",
* "word_count": 9601,
* "created_at": "2022-12-08T02:50:35.662027+00:00",
* "updated_at": "2023-03-22T13:29:41.827456+00:00",
* "published_date": "2022-10-28",
* "notes": "",
* "summary": "The Wuhan Institute of Virology, the cutting-edge ...",
* "image_url": "https://media.vanityfair.com/photos/63599642578d980751943b65/16:9/w_1280,c_limit/vf-1022-covid-trackers-site-story.jpg",
* "parent_id": null,
* "reading_progress": 0,
* }
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public record ReaderItem(
@JsonProperty String id,
@JsonProperty String url,
@JsonProperty("source_url") String sourceUrl,
@JsonProperty String title,
@JsonProperty String author,
@JsonProperty String source,
@JsonProperty Category category,
@JsonProperty Location location,
//@JsonProperty List<String> tags,
@JsonProperty("site_name") String siteName,
@JsonProperty("word_count") int wordCount,
@JsonProperty Instant created_at,
@JsonProperty Instant updated_at,
@JsonProperty Instant published_date,
@JsonProperty String notes,
@JsonProperty String summary,
@JsonProperty("image_url") String imageUrl,
@JsonProperty double reading_progress
) {}
Loading

0 comments on commit 36eb3a1

Please sign in to comment.