voicci 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voicci",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "AI-Powered Audiobook Generator for Claude Code, OpenCode & AI Code Editors. Convert books and PDFs to audiobooks using natural language.",
5
5
  "type": "module",
6
6
  "main": "cli/index.js",
@@ -136,13 +136,16 @@ const EDITORS = {
136
136
  return false;
137
137
  }
138
138
  },
139
- skillsDir: () => path.join(os.homedir(), '.claude', 'skills')
139
+ // Install to both skills/ and commands/ so slash commands appear in autocomplete
140
+ skillsDirs: () => [
141
+ path.join(os.homedir(), '.claude', 'commands'),
142
+ path.join(os.homedir(), '.claude', 'skills')
143
+ ]
140
144
  },
141
145
  'OpenCode': {
142
146
  name: 'OpenCode',
143
147
  detect: () => {
144
148
  const homeDir = os.homedir();
145
- // Check for opencode command or config directory
146
149
  if (fs.existsSync(path.join(homeDir, '.opencode'))) return true;
147
150
  try {
148
151
  execFileSync('which', ['opencode'], { stdio: 'ignore' });
@@ -151,13 +154,12 @@ const EDITORS = {
151
154
  return false;
152
155
  }
153
156
  },
154
- skillsDir: () => path.join(os.homedir(), '.opencode', 'skills')
157
+ skillsDirs: () => [path.join(os.homedir(), '.opencode', 'skills')]
155
158
  },
156
159
  'Cursor': {
157
160
  name: 'Cursor',
158
161
  detect: () => {
159
162
  const homeDir = os.homedir();
160
- // Check for cursor command or config directory
161
163
  if (fs.existsSync(path.join(homeDir, '.cursor'))) return true;
162
164
  if (fs.existsSync(path.join(homeDir, 'Library', 'Application Support', 'Cursor'))) return true;
163
165
  try {
@@ -167,25 +169,23 @@ const EDITORS = {
167
169
  return false;
168
170
  }
169
171
  },
170
- skillsDir: () => {
172
+ skillsDirs: () => {
171
173
  const homeDir = os.homedir();
172
- // Try common locations
173
174
  const locations = [
174
175
  path.join(homeDir, '.cursor', 'skills'),
175
176
  path.join(homeDir, 'Library', 'Application Support', 'Cursor', 'skills')
176
177
  ];
177
178
  for (const loc of locations) {
178
179
  const parent = path.dirname(loc);
179
- if (fs.existsSync(parent)) return loc;
180
+ if (fs.existsSync(parent)) return [loc];
180
181
  }
181
- return locations[0]; // Default to first
182
+ return [locations[0]];
182
183
  }
183
184
  },
184
185
  'Windsurf': {
185
186
  name: 'Windsurf',
186
187
  detect: () => {
187
188
  const homeDir = os.homedir();
188
- // Check for windsurf command or config directory
189
189
  if (fs.existsSync(path.join(homeDir, '.windsurf'))) return true;
190
190
  try {
191
191
  execFileSync('which', ['windsurf'], { stdio: 'ignore' });
@@ -194,7 +194,7 @@ const EDITORS = {
194
194
  return false;
195
195
  }
196
196
  },
197
- skillsDir: () => path.join(os.homedir(), '.windsurf', 'skills')
197
+ skillsDirs: () => [path.join(os.homedir(), '.windsurf', 'skills')]
198
198
  }
199
199
  };
200
200
 
@@ -227,26 +227,33 @@ function detectAndInstall() {
227
227
  if (editor.detect()) {
228
228
  console.log(`✓ Found ${editor.name}`);
229
229
 
230
- const skillsDir = editor.skillsDir();
230
+ const dirs = editor.skillsDirs();
231
231
 
232
- // Create skills directory if it doesn't exist
233
- if (!fs.existsSync(skillsDir)) {
234
- fs.mkdirSync(skillsDir, { recursive: true });
235
- }
232
+ let anyNew = false;
233
+ let allExist = true;
234
+
235
+ for (const skillsDir of dirs) {
236
+ // Create directory if it doesn't exist
237
+ if (!fs.existsSync(skillsDir)) {
238
+ fs.mkdirSync(skillsDir, { recursive: true });
239
+ }
236
240
 
237
- // Install both skills
238
- const simpleStatus = installSkillFile(skillsDir, SKILL_NAME_SIMPLE, SKILL_CONTENT_SIMPLE);
239
- const detailedStatus = installSkillFile(skillsDir, SKILL_NAME_DETAILED, SKILL_CONTENT_DETAILED);
241
+ // Install both skills
242
+ const simpleStatus = installSkillFile(skillsDir, SKILL_NAME_SIMPLE, SKILL_CONTENT_SIMPLE);
243
+ const detailedStatus = installSkillFile(skillsDir, SKILL_NAME_DETAILED, SKILL_CONTENT_DETAILED);
240
244
 
241
- const simpleFile = path.join(skillsDir, SKILL_NAME_SIMPLE);
242
- const detailedFile = path.join(skillsDir, SKILL_NAME_DETAILED);
245
+ if (simpleStatus === 'new' || detailedStatus === 'new') {
246
+ anyNew = true;
247
+ allExist = false;
248
+ if (simpleStatus === 'new') console.log(` ↳ Installed: ${path.join(skillsDir, SKILL_NAME_SIMPLE)}`);
249
+ if (detailedStatus === 'new') console.log(` ↳ Installed: ${path.join(skillsDir, SKILL_NAME_DETAILED)}`);
250
+ }
251
+ }
243
252
 
244
- if (simpleStatus === 'exists' && detailedStatus === 'exists') {
253
+ if (!anyNew) {
245
254
  console.log(` ↳ Skills already installed`);
246
255
  installedEditors.push({ name: editor.name, status: 'exists' });
247
256
  } else {
248
- if (simpleStatus === 'new') console.log(` ↳ Installed: ${simpleFile}`);
249
- if (detailedStatus === 'new') console.log(` ↳ Installed: ${detailedFile}`);
250
257
  installedEditors.push({ name: editor.name, status: 'new' });
251
258
  }
252
259
  }
@@ -1,69 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*'
7
-
8
- jobs:
9
- test:
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout code
14
- uses: actions/checkout@v4
15
-
16
- - name: Setup Node.js
17
- uses: actions/setup-node@v4
18
- with:
19
- node-version: '18'
20
-
21
- - name: Install dependencies
22
- run: npm ci
23
-
24
- - name: Run tests
25
- run: npm test
26
-
27
- publish:
28
- needs: test
29
- runs-on: ubuntu-latest
30
-
31
- steps:
32
- - name: Checkout code
33
- uses: actions/checkout@v4
34
-
35
- - name: Setup Node.js
36
- uses: actions/setup-node@v4
37
- with:
38
- node-version: '18'
39
- registry-url: 'https://registry.npmjs.org'
40
-
41
- - name: Install dependencies
42
- run: npm ci
43
-
44
- - name: Publish to npm
45
- run: npm publish
46
- env:
47
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
48
-
49
- - name: Create GitHub Release
50
- uses: actions/create-release@v1
51
- env:
52
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53
- with:
54
- tag_name: ${{ github.ref }}
55
- release_name: Release ${{ github.ref }}
56
- body: |
57
- ## Installation
58
- ```bash
59
- npm install -g voicci
60
- ```
61
-
62
- Or via install script:
63
- ```bash
64
- curl -fsSL https://voicci.com/voicci-cli/install.sh | bash
65
- ```
66
-
67
- See [CHANGELOG.md](https://github.com/voicci/voicci-cli/blob/main/CHANGELOG.md) for details.
68
- draft: false
69
- prerelease: false
@@ -1,33 +0,0 @@
1
- name: Run Tests
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
-
13
- strategy:
14
- matrix:
15
- node-version: [18.x, 20.x]
16
-
17
- steps:
18
- - name: Checkout code
19
- uses: actions/checkout@v4
20
-
21
- - name: Setup Node.js ${{ matrix.node-version }}
22
- uses: actions/setup-node@v4
23
- with:
24
- node-version: ${{ matrix.node-version }}
25
-
26
- - name: Install dependencies
27
- run: npm ci
28
-
29
- - name: Run tests
30
- run: npm test
31
-
32
- - name: Check package can be packed
33
- run: npm pack --dry-run
package/install.sh DELETED
@@ -1,198 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Voicci Installer
4
- # Converts PDF/text files to high-quality audiobooks using XTTS v2 AI
5
-
6
- set -e
7
-
8
- # Colors for output
9
- RED='\033[0;31m'
10
- GREEN='\033[0;32m'
11
- YELLOW='\033[1;33m'
12
- BLUE='\033[0;34m'
13
- NC='\033[0m' # No Color
14
-
15
- echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
16
- echo -e "${BLUE}║ Voicci Installer v1.0 ║${NC}"
17
- echo -e "${BLUE}║ AI Audiobook Generator (XTTS v2) ║${NC}"
18
- echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
19
- echo ""
20
-
21
- # Detect OS
22
- OS="$(uname -s)"
23
- case "${OS}" in
24
- Linux*) OS_TYPE=Linux;;
25
- Darwin*) OS_TYPE=Mac;;
26
- *) OS_TYPE="UNKNOWN:${OS}"
27
- esac
28
-
29
- if [[ "$OS_TYPE" == "UNKNOWN"* ]]; then
30
- echo -e "${RED}✗${NC} Unsupported operating system: ${OS}"
31
- echo "Voicci currently supports macOS and Linux only."
32
- exit 1
33
- fi
34
-
35
- echo -e "${GREEN}✓${NC} Detected OS: ${OS_TYPE}"
36
-
37
- # Check dependencies
38
- echo ""
39
- echo "Checking dependencies..."
40
-
41
- # Check Node.js
42
- if command -v node &> /dev/null; then
43
- NODE_VERSION=$(node --version)
44
- echo -e "${GREEN}✓${NC} Node.js $NODE_VERSION found"
45
- else
46
- echo -e "${RED}✗${NC} Node.js not found"
47
- echo "Please install Node.js 18+ from https://nodejs.org/"
48
- exit 1
49
- fi
50
-
51
- # Check npm
52
- if command -v npm &> /dev/null; then
53
- NPM_VERSION=$(npm --version)
54
- echo -e "${GREEN}✓${NC} npm $NPM_VERSION found"
55
- else
56
- echo -e "${RED}✗${NC} npm not found"
57
- exit 1
58
- fi
59
-
60
- # Check Python
61
- if command -v python3 &> /dev/null; then
62
- PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}')
63
- PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1)
64
- PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2)
65
-
66
- if [[ $PYTHON_MAJOR -ge 3 ]] && [[ $PYTHON_MINOR -ge 9 ]]; then
67
- echo -e "${GREEN}✓${NC} Python $PYTHON_VERSION found"
68
- else
69
- echo -e "${RED}✗${NC} Python 3.9+ required (found $PYTHON_VERSION)"
70
- exit 1
71
- fi
72
- else
73
- echo -e "${RED}✗${NC} Python 3 not found"
74
- echo "Please install Python 3.9+ from https://www.python.org/"
75
- exit 1
76
- fi
77
-
78
- # Check pip
79
- if command -v pip3 &> /dev/null; then
80
- echo -e "${GREEN}✓${NC} pip3 found"
81
- else
82
- echo -e "${RED}✗${NC} pip3 not found"
83
- exit 1
84
- fi
85
-
86
- # Check pdftotext (optional but recommended)
87
- if command -v pdftotext &> /dev/null; then
88
- echo -e "${GREEN}✓${NC} pdftotext found"
89
- else
90
- echo -e "${YELLOW}⚠${NC} pdftotext not found (optional, for PDF support)"
91
- echo "Install with: brew install poppler (Mac) or apt-get install poppler-utils (Linux)"
92
- fi
93
-
94
- # Determine installation directory
95
- if [[ "$OS_TYPE" == "Mac" ]]; then
96
- INSTALL_DIR="$HOME/Library/Application Support/voicci"
97
- else
98
- INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/voicci"
99
- fi
100
-
101
- echo ""
102
- echo -e "Installation directory: ${BLUE}$INSTALL_DIR${NC}"
103
- echo ""
104
-
105
- # Ask for confirmation
106
- read -p "Continue with installation? (y/n) " -n 1 -r
107
- echo
108
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
109
- echo "Installation cancelled."
110
- exit 0
111
- fi
112
-
113
- # Create installation directory
114
- echo ""
115
- echo "Creating directories..."
116
- mkdir -p "$INSTALL_DIR"/{lib,cli,backend}
117
- echo -e "${GREEN}✓${NC} Directories created"
118
-
119
- # Download Voicci files (in production, this would download from GitHub/website)
120
- echo ""
121
- echo "Installing Voicci..."
122
-
123
- # For now, we'll create the files directly
124
- # In production, this would be: curl -fsSL https://voicci.com/voicci-cli/voicci.tar.gz | tar -xz -C "$INSTALL_DIR"
125
-
126
- # Install Node.js dependencies
127
- cd "$INSTALL_DIR"
128
- echo ""
129
- echo "Installing Node.js dependencies..."
130
-
131
- cat > package.json <<'EOF'
132
- {
133
- "name": "voicci",
134
- "version": "1.0.0",
135
- "description": "AI Audiobook Generator using XTTS v2",
136
- "type": "module",
137
- "bin": {
138
- "voicci": "./cli/index.js"
139
- },
140
- "dependencies": {
141
- "commander": "^11.1.0",
142
- "better-sqlite3": "^9.2.2",
143
- "uuid": "^9.0.1",
144
- "ink": "^4.4.1",
145
- "react": "^18.2.0",
146
- "chalk": "^5.3.0"
147
- },
148
- "engines": {
149
- "node": ">=18.0.0"
150
- }
151
- }
152
- EOF
153
-
154
- npm install --silent --no-audit --no-fund 2>&1 | grep -v "npm WARN"
155
- echo -e "${GREEN}✓${NC} Node.js dependencies installed"
156
-
157
- # Install Python dependencies
158
- echo ""
159
- echo "Installing Python dependencies..."
160
- echo "This may take several minutes (downloading XTTS v2 model ~450MB)..."
161
-
162
- pip3 install --quiet --upgrade pip
163
- pip3 install --quiet TTS torch torchaudio
164
-
165
- echo -e "${GREEN}✓${NC} Python dependencies installed"
166
-
167
- # Download XTTS v2 model
168
- echo ""
169
- echo "Downloading XTTS v2 model (this may take a few minutes)..."
170
- python3 -c "from TTS.api import TTS; TTS('tts_models/multilingual/multi-dataset/xtts_v2')" &>/dev/null || true
171
- echo -e "${GREEN}✓${NC} XTTS v2 model downloaded"
172
-
173
- # Create symlink for global access
174
- BIN_DIR="/usr/local/bin"
175
- if [[ -w "$BIN_DIR" ]]; then
176
- ln -sf "$INSTALL_DIR/cli/index.js" "$BIN_DIR/voicci"
177
- echo -e "${GREEN}✓${NC} Global command 'voicci' installed"
178
- else
179
- echo -e "${YELLOW}⚠${NC} Could not create global command (insufficient permissions)"
180
- echo "Add to your PATH manually: export PATH=\"$INSTALL_DIR/cli:\$PATH\""
181
- fi
182
-
183
- # Final message
184
- echo ""
185
- echo -e "${GREEN}╔════════════════════════════════════════╗${NC}"
186
- echo -e "${GREEN}║ Installation Complete! 🎉 ║${NC}"
187
- echo -e "${GREEN}╚════════════════════════════════════════╝${NC}"
188
- echo ""
189
- echo "Usage:"
190
- echo -e " ${BLUE}voicci mybook.pdf${NC} - Convert PDF to audiobook"
191
- echo -e " ${BLUE}voicci -s${NC} - Check all job statuses"
192
- echo -e " ${BLUE}voicci -l${NC} - List completed audiobooks"
193
- echo -e " ${BLUE}voicci -o <jobId>${NC} - Open audiobook folder"
194
- echo ""
195
- echo "Installation location: $INSTALL_DIR"
196
- echo ""
197
- echo "Get started: voicci --help"
198
- echo ""
@@ -1,263 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { exec } from 'child_process';
4
- import { promisify } from 'util';
5
- import fs from 'fs';
6
- import path from 'path';
7
- import config from './config.js';
8
-
9
- const execAsync = promisify(exec);
10
-
11
- class BookFinder {
12
- constructor() {
13
- this.sources = [
14
- {
15
- name: 'LibGen',
16
- searchUrl: 'http://libgen.rs/search.php',
17
- priority: 1
18
- },
19
- {
20
- name: 'AnnaArchive',
21
- searchUrl: 'https://annas-archive.org',
22
- priority: 2
23
- },
24
- {
25
- name: 'ZLib',
26
- searchUrl: 'https://z-lib.gs',
27
- priority: 3
28
- }
29
- ];
30
-
31
- this.downloadDir = path.join(config.paths.temp, 'downloads');
32
- fs.mkdirSync(this.downloadDir, { recursive: true });
33
- }
34
-
35
- async search(query, limit = 5) {
36
- console.log(`Searching ${this.sources.length} sources...`);
37
-
38
- // Try each source in priority order
39
- for (const source of this.sources.sort((a, b) => a.priority - b.priority)) {
40
- try {
41
- console.log(` Trying ${source.name}...`);
42
- const results = await this.searchSource(source, query);
43
-
44
- if (results.length > 0) {
45
- console.log(` ✓ Found ${results.length} results from ${source.name}`);
46
- return results.slice(0, limit);
47
- }
48
- } catch (error) {
49
- console.log(` ✗ ${source.name} failed: ${error.message}`);
50
- continue;
51
- }
52
- }
53
-
54
- return [];
55
- }
56
-
57
- async searchSource(source, query) {
58
- // Use curl with specific search endpoints
59
- if (source.name === 'LibGen') {
60
- return await this.searchLibGen(query);
61
- } else if (source.name === 'AnnaArchive') {
62
- return await this.searchAnnaArchive(query);
63
- } else if (source.name === 'ZLib') {
64
- return await this.searchZLib(query);
65
- }
66
-
67
- return [];
68
- }
69
-
70
- async searchLibGen(query) {
71
- const searchQuery = encodeURIComponent(query);
72
- const url = `http://libgen.rs/search.php?req=${searchQuery}&res=100&view=simple&phrase=1&column=def`;
73
-
74
- try {
75
- // Use curl to fetch search results
76
- const { stdout } = await execAsync(`curl -s -L "${url}"`);
77
-
78
- // Parse HTML to extract book info
79
- const results = this.parseLibGenHTML(stdout);
80
- return results.map(r => ({ ...r, source: 'LibGen' }));
81
- } catch (error) {
82
- throw new Error(`LibGen search failed: ${error.message}`);
83
- }
84
- }
85
-
86
- parseLibGenHTML(html) {
87
- const results = [];
88
-
89
- // Simple regex parsing (not ideal but works for basic cases)
90
- // Look for table rows with book info
91
- const rowPattern = /<tr[^>]*>.*?<\/tr>/gs;
92
- const rows = html.match(rowPattern) || [];
93
-
94
- for (const row of rows.slice(1)) { // Skip header row
95
- try {
96
- // Extract title
97
- const titleMatch = row.match(/<a[^>]*title="([^"]*)"[^>]*>([^<]+)<\/a>/);
98
- const title = titleMatch ? (titleMatch[1] || titleMatch[2]) : null;
99
-
100
- // Extract author
101
- const authorMatch = row.match(/<td[^>]*>(.*?)<\/td>/g);
102
- const author = authorMatch && authorMatch[1]
103
- ? authorMatch[1].replace(/<[^>]*>/g, '').trim()
104
- : null;
105
-
106
- // Extract download link
107
- const linkMatch = row.match(/href="([^"]*md5=[^"]*)"/);
108
- const downloadPage = linkMatch ? linkMatch[1] : null;
109
-
110
- if (title && downloadPage) {
111
- results.push({
112
- title: title.trim(),
113
- author: author || 'Unknown',
114
- downloadUrl: downloadPage.startsWith('http')
115
- ? downloadPage
116
- : `http://libgen.rs/${downloadPage}`,
117
- format: 'pdf'
118
- });
119
- }
120
- } catch (e) {
121
- // Skip malformed rows
122
- continue;
123
- }
124
- }
125
-
126
- return results.slice(0, 10);
127
- }
128
-
129
- async searchAnnaArchive(query) {
130
- const searchQuery = encodeURIComponent(query);
131
- const url = `https://annas-archive.org/search?q=${searchQuery}`;
132
-
133
- try {
134
- const { stdout } = await execAsync(`curl -s -L "${url}"`);
135
-
136
- // Parse Anna's Archive results
137
- const results = this.parseAnnaArchiveHTML(stdout);
138
- return results.map(r => ({ ...r, source: 'AnnaArchive' }));
139
- } catch (error) {
140
- throw new Error(`Anna's Archive search failed: ${error.message}`);
141
- }
142
- }
143
-
144
- parseAnnaArchiveHTML(html) {
145
- const results = [];
146
-
147
- // Anna's Archive has a JSON API we could use instead
148
- // For now, basic HTML parsing
149
- const linkPattern = /<a[^>]*href="\/md5\/([^"]*)"[^>]*>(.*?)<\/a>/gs;
150
- const matches = html.matchAll(linkPattern);
151
-
152
- for (const match of matches) {
153
- const md5 = match[1];
154
- const title = match[2].replace(/<[^>]*>/g, '').trim();
155
-
156
- if (title && md5) {
157
- results.push({
158
- title,
159
- author: 'Unknown',
160
- downloadUrl: `https://annas-archive.org/md5/${md5}`,
161
- format: 'pdf'
162
- });
163
- }
164
- }
165
-
166
- return results.slice(0, 10);
167
- }
168
-
169
- async searchZLib(query) {
170
- // Z-Library often requires authentication
171
- // For now, return empty results
172
- return [];
173
- }
174
-
175
- async download(book) {
176
- console.log(`Downloading from ${book.source}...`);
177
-
178
- // Generate safe filename
179
- const filename = book.title
180
- .replace(/[^a-z0-9]/gi, '_')
181
- .toLowerCase()
182
- .substring(0, 100) + '.pdf';
183
-
184
- const outputPath = path.join(this.downloadDir, filename);
185
-
186
- if (book.source === 'LibGen') {
187
- await this.downloadFromLibGen(book.downloadUrl, outputPath);
188
- } else if (book.source === 'AnnaArchive') {
189
- await this.downloadFromAnnaArchive(book.downloadUrl, outputPath);
190
- } else {
191
- throw new Error(`Download not supported for source: ${book.source}`);
192
- }
193
-
194
- return outputPath;
195
- }
196
-
197
- async downloadFromLibGen(pageUrl, outputPath) {
198
- // First, fetch the download page to get actual PDF link
199
- const { stdout: pageDirect } = await execAsync(`curl -s -L "${pageUrl}"`);
200
-
201
- // Look for direct download link
202
- const linkMatch = pageHtml.match(/href="(https?:\/\/[^"]*\.pdf)"/i);
203
-
204
- if (!linkMatch) {
205
- // Try alternative download sources
206
- const altMatch = pageHtml.match(/<a[^>]*href="([^"]*)"[^>]*>GET<\/a>/i);
207
-
208
- if (altMatch) {
209
- const downloadUrl = altMatch[1];
210
- await execAsync(`curl -L -o "${outputPath}" "${downloadUrl}"`);
211
- return;
212
- }
213
-
214
- throw new Error('Could not find PDF download link');
215
- }
216
-
217
- const pdfUrl = linkMatch[1];
218
-
219
- // Download PDF
220
- await execAsync(`curl -L -o "${outputPath}" "${pdfUrl}"`);
221
-
222
- // Verify download
223
- if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size < 1000) {
224
- throw new Error('Download failed or file too small');
225
- }
226
- }
227
-
228
- async downloadFromAnnaArchive(pageUrl, outputPath) {
229
- // Fetch download page
230
- const { stdout: pageHtml } = await execAsync(`curl -s -L "${pageUrl}"`);
231
-
232
- // Look for download button/link
233
- const linkMatch = pageHtml.match(/href="([^"]*download[^"]*)"/i);
234
-
235
- if (!linkMatch) {
236
- throw new Error('Could not find download link');
237
- }
238
-
239
- const downloadUrl = linkMatch[1].startsWith('http')
240
- ? linkMatch[1]
241
- : `https://annas-archive.org${linkMatch[1]}`;
242
-
243
- // Download
244
- await execAsync(`curl -L -o "${outputPath}" "${downloadUrl}"`);
245
-
246
- // Verify
247
- if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size < 1000) {
248
- throw new Error('Download failed or file too small');
249
- }
250
- }
251
-
252
- async verify(filePath) {
253
- // Verify PDF is valid
254
- try {
255
- const { stdout } = await execAsync(`pdfinfo "${filePath}"`);
256
- return stdout.includes('Pages:');
257
- } catch (error) {
258
- return false;
259
- }
260
- }
261
- }
262
-
263
- export default BookFinder;