A universal TypeScript library for accessing and downloading Maktabah Shamela v4 APIs. The package runs in both Node.js and modern browsers, providing ergonomic helpers to interact with the Shamela API, download master and book databases, and retrieve book data programmatically.
Warning
This library requires an API key to access Shamela's APIs. I cannot provide API keys and am unable to assist with API key requests.
Please do not:
- Open issues asking for API keys
- Contact me directly for API access
For API key inquiries, please email: mail@shamela.ws
- 🚀 Full data lifecycle – fetch metadata, download master and book databases, and query the results entirely in-memory.
- 🔐 Runtime configuration – configure API credentials, WASM paths, and custom fetch/logging implementations at runtime.
- 🧠 Content tooling – parse, sanitise, and post-process Arabic book content with utilities tailored for Shamela formatting.
- 🌐 Environment aware – automatically selects optimal sql.js WASM bundles for Node.js, browsers, and bundled runtimes.
- 🧪 Well-tested – comprehensive unit and end-to-end coverage to ensure reliable integrations.
- Features
- Installation
- Quick Start
- API Reference
- Examples
- Data Structures
- Next.js Demo
- Troubleshooting
- Testing
- License
npm install shamelabun add shamelayarn add shamelapnpm install shamelaFor simple Node.js scripts (non-bundled environments), the library auto-detects the WASM file:
import { configure, getBook } from 'shamela';
// Configure API credentials
configure({
apiKey: process.env.SHAMELA_API_KEY,
// Configure only the endpoints you need:
booksEndpoint: process.env.SHAMELA_BOOKS_ENDPOINT, // Required for book APIs
masterPatchEndpoint: process.env.SHAMELA_MASTER_ENDPOINT, // Required for master APIs
// sqlJsWasmUrl is auto-detected in standard Node.js
});
// Use the library
const book = await getBook(26592);
console.log(`Downloaded book with ${book.pages.length} pages`);For Next.js, webpack, Turbopack, and other bundlers, you need to explicitly configure the WASM file path.
1. Update next.config.ts or next.config.js:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
serverExternalPackages: ['shamela', 'sql.js'],
// ... rest of your config
};
export default nextConfig;2. Create a server-only configuration file:
// lib/shamela-server.ts
import { configure } from 'shamela';
import { join } from 'node:path';
configure({
sqlJsWasmUrl: join(process.cwd(), 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
apiKey: process.env.SHAMELA_API_KEY,
booksEndpoint: process.env.SHAMELA_BOOKS_ENDPOINT,
masterPatchEndpoint: process.env.SHAMELA_MASTER_ENDPOINT,
});
export { downloadBook, getBook, getBookMetadata, getMaster, downloadMasterDatabase } from 'shamela';3. Use in Server Actions:
'use server';
import { getBookMetadata, downloadBook } from '@/lib/shamela-server';
export async function downloadBookAction(bookId: number) {
const metadata = await getBookMetadata(bookId);
return await downloadBook(bookId, {
bookMetadata: metadata,
outputFile: { path: `./books/${bookId}.db` }
});
}Important: Only import shamela in server-side code (Server Actions, API Routes, or Server Components). Never import in client components or layout.tsx.
In browsers, the library automatically uses a CDN-hosted WASM file:
import { configure, getBook } from 'shamela';
configure({
apiKey: 'your-api-key',
booksEndpoint: 'https://SHAMELA_INSTANCE.ws/api/books',
masterPatchEndpoint: 'https://SHAMELA_INSTANCE.ws/api/master_patch',
// Automatically uses CDN: https://cdn.jsdelivr.net/npm/sql.js@1.13.0/dist/sql-wasm.wasm
});
const book = await getBook(26592);If you only need the content processing utilities (sanitization, parsing, etc.) without the database functionality, use the lightweight shamela/content export:
import {
mapPageCharacterContent,
splitPageBodyFromFooter,
removeTagsExceptSpan,
parseContentRobust,
} from 'shamela/content';
// Process content without loading sql.js (~1.5KB gzipped vs ~900KB)
const clean = removeTagsExceptSpan(mapPageCharacterContent(rawContent));
const [body, footnotes] = splitPageBodyFromFooter(clean);This is ideal for:
- Client-side React/Next.js components
- Bundled environments where you want to avoid sql.js WASM
- Processing pre-downloaded book data
Available exports from shamela/content:
parseContentRobust- Parse HTML into structured lines preserving title spansmapPageCharacterContent- Normalize Arabic text with mapping rulessplitPageBodyFromFooter- Separate body from footnotesremoveArabicNumericPageMarkers- Remove page markersremoveTagsExceptSpan- Strip HTML except spanshtmlToMarkdown- Convert Shamela HTML to Markdown (title spans →##headers)normalizeHtml- Normalize hadeeth tags to standard spansnormalizeLineEndings- Normalize line endings to Unix-style (\n)stripHtmlTags- Strip all HTML tags from contentnormalizeTitleSpans- Handle consecutive title spans (merge, split, or hierarchy)moveContentAfterLineBreakIntoSpan- Move pre-title text into the spanconvertContentToMarkdown- Full pipeline: normalize spans → move pre-title text → convert to Markdown
Available exports from shamela/transform:
denormalizeBooks- Resolve relationships in MasterData to return rich book objects
You can import DEFAULT_MAPPING_RULES from shamela/constants to extend or customize the character mapping used by mapPageCharacterContent:
import { mapPageCharacterContent } from 'shamela/content';
import { DEFAULT_MAPPING_RULES } from 'shamela/constants';
// Extend default rules with custom mappings
const customRules = {
...DEFAULT_MAPPING_RULES,
'customPattern': 'replacement',
};
const processed = mapPageCharacterContent(rawContent, customRules);Initialises runtime configuration including API credentials, custom fetch implementations, sql.js WASM location, and logger overrides.
configure(options: ConfigureOptions): voidExample:
import { configure } from 'shamela';
configure({
apiKey: process.env.SHAMELA_API_KEY!,
booksEndpoint: process.env.SHAMELA_BOOKS_ENDPOINT!,
masterPatchEndpoint: process.env.SHAMELA_MASTER_ENDPOINT!,
});Clears runtime overrides and restores the default silent logger.
resetConfig(): voidUse this in tests or long-running processes when you need a clean configuration slate.
Returns the merged configuration snapshot combining runtime overrides with environment variables.
getConfig(): ShamelaConfigReads a single configuration value without throwing when it is missing.
getConfigValue<Key extends ShamelaConfigKey>(key: Key): ShamelaConfig[Key] | undefinedRetrieves a configuration entry and throws an error if the value is not present.
requireConfigValue(key: Exclude<ShamelaConfigKey, 'fetchImplementation'>): stringFetches metadata for the master database, including download URLs for the latest patches.
getMasterMetadata(version?: number): Promise<GetMasterMetadataResponsePayload>version(optional): The version number to check for updates (defaults to 0)
const metadata = await getMasterMetadata();
console.log(metadata.url); // Download URL
console.log(metadata.version); // Version number
// Check for updates from a specific version
const updates = await getMasterMetadata(5);Downloads the master database containing all books, authors, and categories and writes it to disk or a custom writer.
downloadMasterDatabase(options: DownloadMasterOptions): Promise<string>options.masterMetadata(optional): Pre-fetched metadata to avoid an extra HTTP calloptions.outputFile.path: Output file path (.db,.sqlite, or.json)
// Download as SQLite database
await downloadMasterDatabase({
outputFile: { path: './master.db' }
});
// Download as JSON
await downloadMasterDatabase({
outputFile: { path: './master.json' }
});Fetches metadata for a specific book, including patch release information.
getBookMetadata(id: number, options?: GetBookMetadataOptions): Promise<GetBookMetadataResponsePayload>id: Book identifieroptions.majorVersion(optional): Major version to checkoptions.minorVersion(optional): Minor version to check
const metadata = await getBookMetadata(26592);
console.log(metadata.majorReleaseUrl);
console.log(metadata.minorReleaseUrl);Downloads and processes a book from Shamela, writing it to JSON or SQLite on disk.
downloadBook(id: number, options: DownloadBookOptions): Promise<string>id: Book identifieroptions.bookMetadata(optional): Pre-fetched metadata to avoid re-fetchingoptions.outputFile.path: Output file path (.db,.sqlite, or.json)
// Download as JSON
await downloadBook(26592, {
outputFile: { path: './book.json' }
});
// Download as SQLite
await downloadBook(26592, {
outputFile: { path: './book.db' }
});Generates the URL for a book's cover image using the configured Shamela host.
getCoverUrl(bookId: number): stringconst coverUrl = getCoverUrl(26592);
// Returns: "https://shamela.ws/covers/26592.jpg"Retrieves complete book data as a JavaScript object, returning pages and title entries.
getBook(id: number): Promise<BookData>const book = await getBook(26592);
console.log(book.pages.length);
console.log(book.titles?.length);
console.log(book.pages[0].content);Retrieves the entire master dataset as a JavaScript object, including version information.
getMaster(): Promise<MasterData>const master = await getMaster();
console.log(master.version);
console.log(master.books.length);
console.log(master.authors.length);
console.log(master.categories.length);Resolves the relationships within the MasterData object, returning a flat list of books where author and category IDs have been replaced with their respective objects. It also parses complex fields like metadata and pdf_links.
denormalizeBooks(master: MasterData): DenormalizedBook[]const master = await getMaster();
const books = denormalizeBooks(master);
const library = books[0];
console.log(library.name);
console.log(library.author.name);
console.log(library.category.name);
console.log(library.metadata.date);Parses Shamela HTML snippets into structured lines while preserving title hierarchy and Arabic punctuation.
parseContentRobust(content: string): Line[]const lines = parseContentRobust(rawHtml);
lines.forEach((line) => console.log(line.id, line.text));Normalises page content by applying regex-based replacement rules tuned for Shamela sources.
mapPageCharacterContent(text: string, rules?: Record<string, string>): stringSeparates page body content from trailing footnotes using the default Shamela marker.
splitPageBodyFromFooter(content: string, marker?: string): readonly [string, string]Removes Arabic numeral markers enclosed in ⦗ ⦘, commonly used to denote page numbers.
removeArabicNumericPageMarkers(text: string): stringStrips anchor and hadeeth tags while preserving nested <span> elements.
removeTagsExceptSpan(content: string): stringNormalizes line endings to Unix-style (\n). Converts Windows (\r\n) and old Mac (\r) line endings for consistent pattern matching across platforms.
normalizeLineEndings(content: string): stringStrips all HTML tags from content, keeping only the text.
stripHtmlTags(html: string): stringConverts Shamela HTML to Markdown format. Title spans (<span data-type="title">) become ## headers, narrator links are stripped but text is preserved.
htmlToMarkdown(html: string): stringNormalizes Shamela HTML for CSS styling by converting <hadeeth-N> tags to <span class="hadeeth"> and closing </hadeeth> to </span>.
normalizeHtml(html: string): stringNormalizes consecutive Shamela-style title spans. Shamela exports sometimes contain adjacent title spans that would produce multiple headings on one line when converted to Markdown.
Some Shamela HTML exports contain adjacent title spans like:
<span data-type="title">باب الميم</span><span data-type="title">من اسمه محمد</span>
If you convert each title span to a markdown header, you can end up with ## باب الميم ## من اسمه محمد.
Use normalizeTitleSpans(html, { strategy }) (exported from the library) before converting title spans to markdown:
import { normalizeTitleSpans } from 'flappa-doormal';
const html = '<span data-type="title">باب الميم</span><span data-type="title">من اسمه محمد</span>';
const normalized = normalizeTitleSpans(html, { strategy: 'splitLines' });
// => "<span data-type=\"title\">باب الميم</span>\n<span data-type=\"title\">من اسمه محمد</span>"Strategies:
splitLines: each title span becomes its own line (recommended default)merge: merge adjacent titles into one (join with a separator)hierarchy: keep first as title, convert subsequent todata-type="subtitle"(you decide how to map subtitles in your converter)
normalizeTitleSpans(html: string, options: NormalizeTitleSpanOptions): stringOptions:
strategy: 'merge'- Combines adjacent titles into a single span with a separatorstrategy: 'splitLines'- Places each title on its own linestrategy: 'hierarchy'- Converts subsequent titles to subtitles (data-type="subtitle")separator(optional) - Separator used when merging (default:—)
Moves text that appears after a line break but before a title span into the span. Useful when chapter numbers or prefixes are placed outside the title span in the source HTML.
moveContentAfterLineBreakIntoSpan(html: string): string// Input: "\r١ - <span data-type="title">الباب الأول</span>"
// Output: "\r<span data-type="title">١ - الباب الأول</span>"Converts Shamela HTML to Markdown using the recommended transformation pipeline:
- Normalizes consecutive title spans
- Moves pre-title text into spans
- Converts to Markdown format
convertContentToMarkdown(content: string, options?: NormalizeTitleSpanOptions): stringconst html = '<span data-type="title">كتاب</span><span data-type="title">الإيمان</span>';
const md = convertContentToMarkdown(html);
// => "## كتاب\n## الإيمان"Constructs authenticated API URLs with query parameters and optional API key injection.
buildUrl(endpoint: string, queryParams: Record<string, any>, useAuth?: boolean): URLMakes HTTPS GET requests using the configured fetch implementation, automatically parsing JSON responses and returning binary data otherwise.
httpsGet<T extends Uint8Array | Record<string, any>>(url: string | URL, options?: { fetchImpl?: typeof fetch }): Promise<T>import { downloadMasterDatabase } from 'shamela';
// Download as SQLite
const dbPath = await downloadMasterDatabase({
outputFile: { path: './shamela_master.db' }
});
console.log(`Downloaded to: ${dbPath}`);
// Download as JSON
const jsonPath = await downloadMasterDatabase({
outputFile: { path: './shamela_master.json' }
});import { downloadBook, getBookMetadata } from 'shamela';
const bookId = 26592;
// Download book
await downloadBook(bookId, {
outputFile: { path: `./book_${bookId}.db` }
});
// With pre-fetched metadata
const metadata = await getBookMetadata(bookId);
await downloadBook(bookId, {
bookMetadata: metadata,
outputFile: { path: `./book_${bookId}.json` }
});import { getBook } from 'shamela';
const book = await getBook(26592);
console.log(`Book has ${book.pages.length} pages`);
// Display table of contents
book.titles?.forEach(title => {
console.log(`${title.id}: ${title.content} (Page ${title.page})`);
});
// Access page content
const firstPage = book.pages[0];
console.log(firstPage.content.substring(0, 100));import { getCoverUrl, getMaster } from 'shamela';
const master = await getMaster();
// Generate cover URLs for all books
master.books.forEach(book => {
const coverUrl = getCoverUrl(book.id);
console.log(`${book.name}: ${coverUrl}`);
});type BookData = {
pages: Page[];
titles: Title[];
};type MasterData = {
authors: Author[];
books: Book[];
categories: Category[];
version: number;
};type Page = {
id: number;
content: string;
part?: string;
page?: number;
number?: string;
};type Title = {
id: number;
content: string;
page: number;
parent?: number;
};parseContentRobust(content: string): Converts Shamela page HTML into structured linesmapPageCharacterContent(content: string): Removes footnote markers and normalizes textsplitPageBodyFromFooter(content: string): Separates page content from footnotesremoveArabicNumericPageMarkers(text: string): Removes Arabic page number markersremoveTagsExceptSpan(content: string): Strips HTML tags except span elements
A minimal Next.js 16 demo application is available in the demo/ directory.
Setup:
Create demo/.env.local:
SHAMELA_API_KEY=your_api_key
SHAMELA_API_MASTER_PATCH_ENDPOINT=https://SHAMELA_INSTANCE.ws/api/master_patch
SHAMELA_API_BOOKS_ENDPOINT=https://SHAMELA_INSTANCE.ws/api/booksRun:
bun run demo # Development
bun run demo:build # Production build
bun run demo:start # Production serverVisit http://localhost:3000 to explore the API.
This occurs in bundled environments (Next.js, webpack, Turbopack).
Solution: Add explicit configuration:
import { configure } from 'shamela';
import { join } from 'node:path';
configure({
sqlJsWasmUrl: join(process.cwd(), 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
apiKey: process.env.SHAMELA_API_KEY,
booksEndpoint: process.env.SHAMELA_BOOKS_ENDPOINT,
masterPatchEndpoint: process.env.SHAMELA_MASTER_ENDPOINT,
});-
Add to
next.config.ts:serverExternalPackages: ['shamela', 'sql.js']
-
Only import shamela in server-side code
-
Create a separate
lib/shamela-server.tsfor configuration
Ensure serverExternalPackages is set in your Next.js config for both development and production.
Adjust the WASM path based on your structure:
configure({
sqlJsWasmUrl: join(process.cwd(), '../../node_modules/sql.js/dist/sql-wasm.wasm'),
// ... other config
});Run tests with Bun:
bun test src # Unit tests
bun run e2e # End-to-end tests
bun run test:exports # Build-output validation
bun run format # Format code
bun run lint # Lint codeThe scripts/ directory contains standalone reverse-engineering tools for extracting and decoding data from Shamela's desktop application databases. These are development tools, not part of the published npm package.
| Script | Purpose |
|---|---|
shamela-decoder.ts |
Core decoder for Shamela's custom character encoding |
export-narrators.ts |
Exports 18,989 narrator biographies from S1.db |
export-roots.ts |
Exports 3.2M Arabic word→root morphological mappings from S2.db |
# Export narrators to JSON
bun run scripts/export-narrators.ts
# Export Arabic roots
bun run scripts/export-roots.ts
# Run script tests
bun test scripts/scripts/README.md– Quick start guide and reverse-engineering methodologyscripts/AGENTS.md– Comprehensive documentation including:- Database schema discoveries
- Character encoding algorithm (substitution cipher)
- Validation approaches and coverage statistics
- Common patterns and debugging techniques
MIT License - see LICENSE file for details.