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 コンテンツが入る (省略)..."
  }