Readwise Reader API を用いて、保存した記事を取得するスクリプトの例です。
実行コマンド
- Node.js v23 以上で直接実行
.env
ファイルにREADWISE_API_ACCESS_TOKEN
をセットする
node --env-file .env ./fetch-readwise-articles.ts
コード
outputDir
は適宜指定してください
import * as fs from "fs";
import * as path from "path";
/**
* Readwise API v3 List endpoint query parameters.
* See https://readwise.io/reader_api for more details.
*/
type ReadwiseDocumentListParams = {
/**
* The document's unique id. Using this parameter it will return just one document, if found.
*/
id?: string;
/**
* Fetch only documents updated after this date (formatted as ISO 8601 date).
*/
updatedAfter?: string;
/**
* The document's location.
*/
location?: "new" | "later" | "shortlist" | "archive" | "feed";
/**
* The document's category.
*/
category?:
| "article"
| "email"
| "rss"
| "highlight"
| "note"
| "pdf"
| "epub"
| "tweet"
| "video";
/**
* 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.
*/
pageCursor?: string;
/**
* Include the html_content field in each document's data.
* Please note that enabling this feature may slightly increase request processing time.
*/
withHtmlContent?: boolean;
};
/**
* Represents a single document returned by the Readwise API v3 List endpoint.
*/
type ReadwiseDocument = {
id: string;
url: string;
source_url: string | null;
title: string;
author: string | null;
source: string;
category:
| "article"
| "email"
| "rss"
| "highlight"
| "note"
| "pdf"
| "epub"
| "tweet"
| "video";
location: "new" | "later" | "shortlist" | "archive" | "feed";
tags: Record<string, any>; // Assuming tags is an object, adjust if needed
site_name: string | null;
word_count: number | null;
created_at: string; // ISO 8601 date string
updated_at: string; // ISO 8601 date string
notes: string | null;
published_date: string | null; // Date string (YYYY-MM-DD)
summary: string | null;
image_url: string | null;
parent_id: string | null;
reading_progress: number; // Assuming float between 0 and 1
first_opened_at: string | null; // ISO 8601 date string
last_opened_at: string | null; // ISO 8601 date string
saved_at: string; // ISO 8601 date string
last_moved_at: string; // ISO 8601 date string
html_content?: string; // Only present if withHtmlContent=true
};
/**
* Represents the response structure of the Readwise API v3 List endpoint.
*/
type ReadwiseDocumentListResponse = {
count: number;
nextPageCursor: string | null;
results: ReadwiseDocument[];
};
const fetchDocumentListApi = async (
updatedAfter: string | null = null,
location: ReadwiseDocumentListParams["location"] = "later",
withHtmlContent: boolean = false
): Promise<ReadwiseDocument[]> => {
let fullData: ReadwiseDocument[] = [];
let nextPageCursor: string | null = null;
while (true) {
const queryParams = new URLSearchParams();
if (nextPageCursor) {
queryParams.append("pageCursor", nextPageCursor);
}
if (updatedAfter) {
queryParams.append("updatedAfter", updatedAfter);
}
if (location) {
queryParams.append("location", location);
}
if (withHtmlContent) {
queryParams.append("withHtmlContent", "true");
}
console.log(
"Making export api request with params " + queryParams.toString()
);
const response = await fetch(
"https://readwise.io/api/v3/list/?" + queryParams.toString(),
{
method: "GET",
headers: {
Authorization: `Token ${process.env.READWISE_API_ACCESS_TOKEN}`,
},
}
);
const responseJson: ReadwiseDocumentListResponse = await response.json();
fullData.push(...responseJson.results);
nextPageCursor = responseJson.nextPageCursor;
if (!nextPageCursor) {
break;
}
}
return fullData;
};
const main = async () => {
// Get all of a user's documents in the 'later' location
const updatedAfter = "2025-04-14T00:00:00+09:00"; // 必要に応じて更新日時を調整
const laterData = await fetchDocumentListApi(updatedAfter, "new", true);
// Group articles by updated_at date (YYYY-MM-DD) based on local timezone
const groupedData: Map<string, ReadwiseDocument[]> = new Map();
for (const article of laterData) {
// Convert ISO 8601 string to Date object
const updatedAtDate = new Date(article.saved_at);
// Get year, month, and day based on local timezone
const year = updatedAtDate.getFullYear();
// getMonth() returns 0-11, so add 1. Pad with '0' if needed.
const month = (updatedAtDate.getMonth() + 1).toString().padStart(2, '0');
// getDate() returns 1-31. Pad with '0' if needed.
const day = updatedAtDate.getDate().toString().padStart(2, '0');
// Format as YYYY-MM-DD
const date = `${year}-${month}-${day}`;
if (!groupedData.has(date)) {
groupedData.set(date, []);
}
// Non-null assertion because we just set it if it didn't exist
groupedData.get(date)!.push(article);
}
// Define the output path for the JSON files
const outputDir = "./path/to/out/dir";
console.log(`Output directory: ${outputDir}`);
// Ensure the output directory exists
if (!fs.existsSync(outputDir)) {
try {
fs.mkdirSync(outputDir, { recursive: true });
console.log(`Created directory: ${outputDir}`);
} catch (error) {
console.error(`Error creating directory ${outputDir}:`, error);
return; // Exit if directory creation fails
}
}
// Save the grouped data to separate JSON files
for (const [date, articlesForDate] of groupedData.entries()) {
const outputPath = path.join(
outputDir,
`readwise_later_articles_${date}.json`
);
try {
fs.writeFileSync(outputPath, JSON.stringify(articlesForDate, null, 2));
console.log(
`Successfully saved later articles for ${date} to ${outputPath}`
);
} catch (error) {
console.error(`Error writing file to ${outputPath}:`, error);
}
}
};
main();
取得される json 例
{
"id": "01jskz8xxxxx",
"url": "https://read.readwise.io/read/xxx",
"title": "How Heroku Is ‘Re-Platforming’ Its Platform",
"author": "Heather Joslyn",
"source": "Reader RSS",
"category": "rss",
"location": "new",
"tags": {},
"site_name": "The New Stack",
"word_count": 604,
"created_at": "2025-04-24T13:19:37.157537+00:00",
"updated_at": "2025-04-25T05:01:57.499511+00:00",
"published_date": "2025-04-24",
"summary": "Building an internal platform, or moving a legacy monolith to microservices and the cloud, can be a huge undertaking. Such\nThe post How Heroku Is ‘Re-Platforming’ Its Platform appeared first on The New Stack.",
"image_url": "https://cdn.thenewstack.io/media/2025/04/56e4407a-kccnc-eu-25_betty-junod_featured-1024x576.png",
"content": null,
"source_url": "https://thenewstack.io/how-heroku-is-re-platforming-its-platform/",
"notes": "",
"parent_id": null,
"reading_progress": 0,
"first_opened_at": "2025-04-25T05:01:28.715000+00:00",
"last_opened_at": "2025-04-25T05:01:28.715000+00:00",
"saved_at": "2025-04-24T13:19:34.098000+00:00",
"last_moved_at": "2025-04-25T05:01:17.234000+00:00",
"html_content": "...HTML コンテンツが入る (省略)..."
}