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 +1 -1
- package/scripts/postinstall.js +30 -23
- package/.github/workflows/publish.yml +0 -69
- package/.github/workflows/test.yml +0 -33
- package/install.sh +0 -198
- package/lib/book-finder-old.js.backup +0 -263
- package/lib/queue-sqlite.js.backup +0 -242
- package/lib/text-cleaner-v2.js +0 -612
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voicci",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -136,13 +136,16 @@ const EDITORS = {
|
|
|
136
136
|
return false;
|
|
137
137
|
}
|
|
138
138
|
},
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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];
|
|
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
|
-
|
|
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
|
|
230
|
+
const dirs = editor.skillsDirs();
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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 (
|
|
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;
|