Add Open Library API integration for missing book metadata

This commit is contained in:
Roni Laukkarinen
2026-02-01 18:09:56 +02:00
parent 170bb786de
commit 3e23442f26
4 changed files with 109 additions and 4 deletions
+4
View File
@@ -1,3 +1,7 @@
### 2026-02-01: 1.0.3
* Add Open Library API integration for missing book metadata (opt-in)
### 2026-02-01: 1.0.2 ### 2026-02-01: 1.0.2
* Convert book metadata to semantic HTML for accessibility * Convert book metadata to semantic HTML for accessibility
+3 -1
View File
@@ -1,6 +1,6 @@
# ⚡ Speed reader # ⚡ Speed reader
![Version](https://img.shields.io/badge/version-1.0.2-blue?style=for-the-badge) ![Version](https://img.shields.io/badge/version-1.0.3-blue?style=for-the-badge)
![React](https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=black) ![React](https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=black)
![Vite](https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=vite&logoColor=white) ![Vite](https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=vite&logoColor=white)
![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black) ![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black)
@@ -22,6 +22,8 @@ A web-based speed reading tool using Rapid Serial Visual Presentation (RSVP) wit
Everything runs locally in your browser. Your text and files never leave your computer - no servers, no tracking, no analytics. EPUB and TXT files are processed entirely client-side. Settings and reading progress are stored in localStorage. Works offline once loaded. Everything runs locally in your browser. Your text and files never leave your computer - no servers, no tracking, no analytics. EPUB and TXT files are processed entirely client-side. Settings and reading progress are stored in localStorage. Works offline once loaded.
Optional: If enabled in settings, missing book metadata (title, author, cover) can be fetched from [Open Library API](https://openlibrary.org/developers/api). This sends the book title to their servers.
## What is RSVP? ## What is RSVP?
RSVP (Rapid Serial Visual Presentation) is a reading technique that displays text one word at a time at a fixed focal point. This eliminates saccades (eye movements) that normally slow down reading, allowing for significantly faster reading speeds. RSVP (Rapid Serial Visual Presentation) is a reading technique that displays text one word at a time at a fixed focal point. This eliminates saccades (eye movements) that normally slow down reading, allowing for significantly faster reading speeds.
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "rsvp-speed-reader", "name": "rsvp-speed-reader",
"private": true, "private": true,
"version": "1.0.2", "version": "1.0.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+101 -2
View File
@@ -233,6 +233,33 @@ function formatReadingTime(minutes) {
return `${hours}h ${mins}m`; return `${hours}h ${mins}m`;
} }
// Fetch book metadata from Open Library API
async function fetchMetadataFromOpenLibrary(title, author) {
if (!title && !author) return null;
try {
const query = [title, author].filter(Boolean).join(" ");
const url = `https://openlibrary.org/search.json?q=${encodeURIComponent(query)}&limit=1&fields=title,author_name,cover_i`;
const response = await fetch(url);
if (!response.ok) return null;
const data = await response.json();
if (!data.docs || data.docs.length === 0) return null;
const book = data.docs[0];
return {
title: book.title || null,
author: book.author_name?.[0] || null,
cover: book.cover_i
? `https://covers.openlibrary.org/b/id/${book.cover_i}-M.jpg`
: null,
};
} catch (e) {
console.error("Failed to fetch from Open Library:", e);
return null;
}
}
function App() { function App() {
// Load settings only once on mount // Load settings only once on mount
const [savedSettings] = useState(() => loadSettings()); const [savedSettings] = useState(() => loadSettings());
@@ -271,6 +298,9 @@ function App() {
const [wordAmount, setWordAmount] = useState( const [wordAmount, setWordAmount] = useState(
() => savedSettings?.wordAmount ?? 1, () => savedSettings?.wordAmount ?? 1,
); );
const [fetchMetadataOnline, setFetchMetadataOnline] = useState(
() => savedSettings?.fetchMetadataOnline ?? false,
);
const timeoutRef = useRef(null); const timeoutRef = useRef(null);
const prevTextRef = useRef(text); const prevTextRef = useRef(text);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
@@ -284,7 +314,23 @@ function App() {
if (file.name.endsWith(".epub")) { if (file.name.endsWith(".epub")) {
const result = await parseEpub(file); const result = await parseEpub(file);
setText(result.text); setText(result.text);
setBookMetadata(result.metadata);
let metadata = result.metadata;
// Fetch missing metadata from Open Library if enabled
if (fetchMetadataOnline && (!metadata.title || !metadata.cover)) {
const onlineMetadata = await fetchMetadataFromOpenLibrary(
metadata.title,
metadata.author,
);
if (onlineMetadata) {
metadata = {
title: metadata.title || onlineMetadata.title,
author: metadata.author || onlineMetadata.author,
cover: metadata.cover || onlineMetadata.cover,
};
}
}
setBookMetadata(metadata);
} else if (file.name.endsWith(".txt")) { } else if (file.name.endsWith(".txt")) {
const textContent = await file.text(); const textContent = await file.text();
setText(textContent); setText(textContent);
@@ -335,8 +381,9 @@ function App() {
sideOpacity, sideOpacity,
wordAmount, wordAmount,
bookMetadata, bookMetadata,
fetchMetadataOnline,
}); });
}, [wpm, text, currentIndex, sideOpacity, wordAmount, bookMetadata]); }, [wpm, text, currentIndex, sideOpacity, wordAmount, bookMetadata, fetchMetadataOnline]);
const getBaseDelay = useCallback(() => { const getBaseDelay = useCallback(() => {
return (60 / wpm) * 1000; return (60 / wpm) * 1000;
@@ -890,6 +937,31 @@ function App() {
</span> </span>
</div> </div>
</div> </div>
<div style={styles.settingRow}>
<label style={styles.settingLabel}>
<span>Fetch missing metadata online</span>
<span style={styles.settingHint}>Uses Open Library API</span>
</label>
<div style={styles.settingControl}>
<button
onClick={() => setFetchMetadataOnline(!fetchMetadataOnline)}
style={{
...styles.toggleBtn,
backgroundColor: fetchMetadataOnline ? "#ff6b6b" : "#333",
}}
aria-pressed={fetchMetadataOnline}
>
<span
style={{
...styles.toggleKnob,
transform: fetchMetadataOnline
? "translateX(16px)"
: "translateX(0)",
}}
/>
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1243,6 +1315,8 @@ const styles = {
settingLabel: { settingLabel: {
fontSize: "0.875rem", fontSize: "0.875rem",
color: "rgb(98, 98, 98)", color: "rgb(98, 98, 98)",
display: "flex",
flexDirection: "column",
}, },
settingControl: { settingControl: {
display: "flex", display: "flex",
@@ -1270,6 +1344,31 @@ const styles = {
width: "120px", width: "120px",
accentColor: "#ff6b6b", accentColor: "#ff6b6b",
}, },
settingHint: {
display: "block",
fontSize: "0.7rem",
color: "#555",
marginTop: "2px",
},
toggleBtn: {
width: "40px",
height: "24px",
borderRadius: "12px",
border: "none",
cursor: "pointer",
position: "relative",
transition: "background-color 0.2s",
},
toggleKnob: {
position: "absolute",
top: "3px",
left: "3px",
width: "18px",
height: "18px",
borderRadius: "50%",
backgroundColor: "#fff",
transition: "transform 0.2s",
},
shortcutList: { shortcutList: {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",