mirror of
https://github.com/jlengrand/RemarkablePocket.git
synced 2026-03-10 08:41:19 +00:00
Add TokenRefresher
This commit is contained in:
@@ -77,6 +77,7 @@ Synchronizes articles from Pocket to the Remarkable tablet.
|
||||
|
||||
## Limitations
|
||||
- Articles behind a paywall cannot be downloaded.
|
||||
- Articles on websites with sophisticated DDOS protection cannot be downloaded.
|
||||
- Articles that use javascript to load the the content cannot be downloaded.
|
||||
|
||||
## Thanks
|
||||
|
||||
@@ -28,6 +28,7 @@ final class DownloadService {
|
||||
@PostConstruct
|
||||
void createStorageDir() throws IOException {
|
||||
storageDir = Files.createTempDirectory(null);
|
||||
LOG.debug("Created temporary storage directory: {}.", storageDir);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import javax.annotation.PostConstruct;
|
||||
import net.lingala.zip4j.core.ZipFile;
|
||||
import net.lingala.zip4j.exception.ZipException;
|
||||
@@ -18,14 +19,19 @@ import net.lingala.zip4j.io.ZipInputStream;
|
||||
import net.lingala.zip4j.model.FileHeader;
|
||||
import nl.carosi.remarkablepocket.model.DocumentMetadata;
|
||||
import nl.siegmann.epublib.epub.EpubReader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
final class MetadataProvider {
|
||||
private final Jrmapi rmapi;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MetadataProvider.class);
|
||||
|
||||
private final AtomicReference<Jrmapi> rmapi;
|
||||
private final EpubReader epubReader;
|
||||
private final ObjectMapper objectMapper;
|
||||
private Path workDir;
|
||||
|
||||
public MetadataProvider(Jrmapi rmapi, EpubReader epubReader, ObjectMapper objectMapper) {
|
||||
public MetadataProvider(
|
||||
AtomicReference<Jrmapi> rmapi, EpubReader epubReader, ObjectMapper objectMapper) {
|
||||
this.rmapi = rmapi;
|
||||
this.epubReader = epubReader;
|
||||
this.objectMapper = objectMapper;
|
||||
@@ -34,13 +40,15 @@ final class MetadataProvider {
|
||||
@PostConstruct
|
||||
void createWorkDir() throws IOException {
|
||||
workDir = Files.createTempDirectory(null);
|
||||
LOG.debug("Created temporary working directory: {}.", workDir);
|
||||
}
|
||||
|
||||
DocumentMetadata getMetadata(Document doc) {
|
||||
LOG.debug("Getting metadata for document: {}.", doc.getVissibleName());
|
||||
ZipFile zip;
|
||||
String fileHash;
|
||||
try {
|
||||
zip = new ZipFile(rmapi.fetchZip(doc, workDir.toString() + File.separator));
|
||||
zip = new ZipFile(rmapi.get().fetchZip(doc, workDir.toString() + File.separator));
|
||||
fileHash = getFileHash(zip);
|
||||
} catch (ZipException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -6,6 +6,7 @@ import es.jlarriba.jrmapi.Jrmapi;
|
||||
import es.jlarriba.jrmapi.model.Document;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.PostConstruct;
|
||||
import nl.carosi.remarkablepocket.model.DocumentMetadata;
|
||||
@@ -15,13 +16,13 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
final class RemarkableService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RemarkableService.class);
|
||||
private final Jrmapi rmapi;
|
||||
private final AtomicReference<Jrmapi> rmapi;
|
||||
private final MetadataProvider metadataProvider;
|
||||
private final String rmStorageDir;
|
||||
private String rmStorageDirId = "";
|
||||
|
||||
public RemarkableService(
|
||||
Jrmapi rmapi,
|
||||
AtomicReference<Jrmapi> rmapi,
|
||||
MetadataProvider metadataProvider,
|
||||
@Value("${rm.storage-dir}") String rmStorageDir) {
|
||||
if (!rmStorageDir.endsWith("/")) {
|
||||
@@ -40,12 +41,12 @@ final class RemarkableService {
|
||||
String[] dirs = rmStorageDir.substring(1, rmStorageDir.length() - 1).split("/");
|
||||
for (final String dir : dirs) {
|
||||
rmStorageDirId =
|
||||
rmapi.listDocs().stream()
|
||||
rmapi.get().listDocs().stream()
|
||||
.filter(e -> e.getParent().equals(rmStorageDirId))
|
||||
.filter(e -> e.getVissibleName().equals(dir))
|
||||
.findFirst()
|
||||
.map(Document::getID)
|
||||
.orElseGet(() -> rmapi.createDir(dir, rmStorageDirId));
|
||||
.orElseGet(() -> rmapi.get().createDir(dir, rmStorageDirId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,22 +74,22 @@ final class RemarkableService {
|
||||
}
|
||||
|
||||
void delete(Document doc) {
|
||||
rmapi.deleteEntry(doc);
|
||||
rmapi.get().deleteEntry(doc);
|
||||
}
|
||||
|
||||
private void logPages(DocumentMetadata meta) {
|
||||
LOG.debug(
|
||||
"{}: Current page: {}, page count: {}",
|
||||
"{}: Current page: {}, page count: {}.",
|
||||
meta.doc().getVissibleName(),
|
||||
meta.doc().getCurrentPage() + 1,
|
||||
meta.pageCount());
|
||||
}
|
||||
|
||||
private Stream<Document> docsStream() {
|
||||
return rmapi.listDocs().stream().filter(e -> e.getParent().equals(rmStorageDirId));
|
||||
return rmapi.get().listDocs().stream().filter(e -> e.getParent().equals(rmStorageDirId));
|
||||
}
|
||||
|
||||
private void upload(Path path) {
|
||||
rmapi.uploadDoc(path.toFile(), rmStorageDirId);
|
||||
rmapi.get().uploadDoc(path.toFile(), rmStorageDirId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package nl.carosi.remarkablepocket;
|
||||
|
||||
import es.jlarriba.jrmapi.Jrmapi;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import nl.siegmann.epublib.epub.EpubReader;
|
||||
import nl.siegmann.epublib.epub.EpubWriter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -22,10 +23,11 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
PocketService.class,
|
||||
RemarkableService.class,
|
||||
SyncService.class,
|
||||
TokenRefresher.class
|
||||
})
|
||||
public class SyncApplication {
|
||||
@Bean
|
||||
Jrmapi jrmapi(@Value("${rm.device-token}") String deviceToken) {
|
||||
return new Jrmapi(deviceToken);
|
||||
AtomicReference<Jrmapi> jrmapi(@Value("${rm.device-token}") String deviceToken) {
|
||||
return new AtomicReference<>(new Jrmapi(deviceToken));
|
||||
}
|
||||
}
|
||||
|
||||
73
src/main/java/nl/carosi/remarkablepocket/TokenRefresher.java
Normal file
73
src/main/java/nl/carosi/remarkablepocket/TokenRefresher.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package nl.carosi.remarkablepocket;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import es.jlarriba.jrmapi.Jrmapi;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import javax.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
|
||||
// Hacky, but unfortunately jrmapi does not refresh bearer tokens.
|
||||
final class TokenRefresher {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TokenRefresher.class);
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final TaskScheduler scheduler;
|
||||
private final AtomicReference<Jrmapi> rmapi;
|
||||
private final String deviceToken;
|
||||
private final Base64.Decoder decoder = Base64.getDecoder();
|
||||
|
||||
public TokenRefresher(
|
||||
ObjectMapper objectMapper,
|
||||
TaskScheduler scheduler,
|
||||
AtomicReference<Jrmapi> rmapi,
|
||||
@Value("${rm.device-token}") String deviceToken) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.scheduler = scheduler;
|
||||
this.rmapi = rmapi;
|
||||
this.deviceToken = deviceToken;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
void scheduleRefresh() {
|
||||
try {
|
||||
scheduleRefreshImpl();
|
||||
} catch (NoSuchFieldException | IllegalAccessException | JsonProcessingException e) {
|
||||
throw new RuntimeException("Could not refresh jrmapi token.");
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleRefreshImpl()
|
||||
throws NoSuchFieldException, IllegalAccessException, JsonProcessingException {
|
||||
Field tokenField = Jrmapi.class.getDeclaredField("userToken");
|
||||
tokenField.setAccessible(true);
|
||||
String token = (String) tokenField.get(rmapi.get());
|
||||
int exp = getExpiration(token);
|
||||
Instant nextRefresh = Instant.ofEpochSecond(exp - 60 * 10); // 10 min margin.
|
||||
scheduler.schedule(this::refresh, nextRefresh);
|
||||
LOG.debug("Next token refresh scheduled at: {}.", nextRefresh);
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
rmapi.set(new Jrmapi(deviceToken));
|
||||
LOG.debug("Refreshed jrmapi token.");
|
||||
scheduleRefresh();
|
||||
}
|
||||
|
||||
private int getExpiration(String jwt) throws JsonProcessingException {
|
||||
String[] chunks = jwt.split("\\.");
|
||||
String payload = new String(decoder.decode(chunks[1]));
|
||||
JWT exp = objectMapper.readValue(payload, JWT.class);
|
||||
return exp.exp();
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record JWT(int exp) {}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ public final record Article(String id, String url, String title) {
|
||||
}
|
||||
|
||||
private static String sanitize(String title) {
|
||||
return title.replaceAll("/", " ").replaceAll(" +", " ").strip();
|
||||
return title
|
||||
.replaceAll("[‘’\"]", "'")
|
||||
.replaceAll(":", " -")
|
||||
.replaceAll("[/?<>*.|\\\\]", " ")
|
||||
.replaceAll(" +", " ").strip();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user