Compare commits
9 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
17d9e24f98 | |
|
|
da965f4324 | |
|
|
70c68e3593 | |
|
|
d45430c07f | |
|
|
2fabb02f40 | |
|
|
2d669ffac6 | |
|
|
752f9054c2 | |
|
|
a8a6390a55 | |
|
|
8a790043f8 |
96
CLAUDE.md
96
CLAUDE.md
|
|
@ -71,10 +71,10 @@ This is configured in `frontend/vite.config.ts`. Production deployments need a r
|
|||
|
||||
Routes are split by domain:
|
||||
- `backend/src/routes/channels.ts`: Channel CRUD + refresh
|
||||
- `backend/src/routes/videos.ts`: Video CRUD + retry
|
||||
- `backend/src/routes/config.ts`: Settings + scheduler control
|
||||
- `backend/src/routes/videos.ts`: Video CRUD + retry + streaming
|
||||
- `backend/src/routes/config.ts`: Settings + scheduler control + WebDAV test
|
||||
|
||||
Video files are served statically from `backend/downloads/` directory.
|
||||
Video files are served via `/api/videos/:id/stream` endpoint which handles both local storage and WebDAV proxying.
|
||||
|
||||
## Key Constraints
|
||||
|
||||
|
|
@ -103,7 +103,8 @@ Adding a channel fetches ALL videos from that channel using yt-dlp's `--flat-pla
|
|||
## File Locations
|
||||
|
||||
- Database: `backend/data.db`
|
||||
- Downloaded videos: `backend/downloads/{videoId}.mp4`
|
||||
- Downloaded videos (local mode): `backend/downloads/{videoId}.mp4`
|
||||
- Downloaded videos (WebDAV mode): Stored on WebDAV server, proxied through backend
|
||||
- Environment: Optional `backend/.env` (PORT, NODE_ENV)
|
||||
|
||||
## System Requirements
|
||||
|
|
@ -153,3 +154,90 @@ For persistent 403 errors, export cookies from your browser:
|
|||
- Increase download interval to avoid rate limiting
|
||||
- Use variance to randomize download times
|
||||
- Avoid downloading too many videos from the same channel quickly
|
||||
|
||||
## WebDAV Storage
|
||||
|
||||
VidRip supports storing videos on a WebDAV server instead of local storage. This is useful for:
|
||||
- Network-attached storage (NAS) devices
|
||||
- Cloud storage with WebDAV support (Nextcloud, ownCloud)
|
||||
- Remote servers with WebDAV access
|
||||
|
||||
### Enabling WebDAV
|
||||
|
||||
1. Go to Settings page in the UI
|
||||
2. Enable "WebDAV Storage"
|
||||
3. Configure connection details:
|
||||
- **Server URL**: Full WebDAV server URL (e.g., `https://nextcloud.example.com/remote.php/dav/files/username`)
|
||||
- **Username**: WebDAV authentication username
|
||||
- **Password**: WebDAV authentication password
|
||||
- **Directory Path**: Subdirectory on server (default: `/vidrip/`)
|
||||
4. Click "Test Connection" to verify settings
|
||||
5. Save Settings
|
||||
|
||||
### How WebDAV Works
|
||||
|
||||
**Download Flow**:
|
||||
1. Video is downloaded to `backend/downloads/` temporarily
|
||||
2. After successful download, video is uploaded to WebDAV server
|
||||
3. Local temporary file is deleted after successful upload
|
||||
4. Database stores path as `webdav://{videoId}.mp4`
|
||||
|
||||
**Playback Flow**:
|
||||
1. Frontend requests `/api/videos/:id/stream`
|
||||
2. Backend detects WebDAV storage (path starts with `webdav://`)
|
||||
3. Backend creates stream from WebDAV server
|
||||
4. Stream is proxied to frontend (transparent to user)
|
||||
|
||||
**Deletion Flow**:
|
||||
- When deleting a video, backend automatically removes from WebDAV server
|
||||
- Database record is also deleted
|
||||
|
||||
### Supported WebDAV Servers
|
||||
|
||||
Tested with:
|
||||
- **Nextcloud**: Use WebDAV URL format: `https://your-nextcloud.com/remote.php/dav/files/username`
|
||||
- **ownCloud**: Similar to Nextcloud
|
||||
- **Generic WebDAV**: Any RFC-compliant WebDAV server
|
||||
|
||||
### Configuration Details
|
||||
|
||||
WebDAV settings are stored in the `config` table:
|
||||
- `webdavEnabled`: 'true' or 'false'
|
||||
- `webdavUrl`: Server URL
|
||||
- `webdavUsername`: Username
|
||||
- `webdavPassword`: Password (stored in plaintext in database)
|
||||
- `webdavPath`: Directory path on server
|
||||
|
||||
### Troubleshooting WebDAV
|
||||
|
||||
**Connection Test Fails**:
|
||||
- Verify server URL is correct and accessible from backend server
|
||||
- Check username/password are correct
|
||||
- Ensure WebDAV path exists or backend has permission to create it
|
||||
- Check firewall rules if server is remote
|
||||
|
||||
**Upload Fails**:
|
||||
- Check available disk space on WebDAV server
|
||||
- Verify write permissions on directory
|
||||
- Check WebDAV server logs for detailed errors
|
||||
- Ensure network is stable for large uploads
|
||||
|
||||
**Playback Issues**:
|
||||
- Verify WebDAV server is accessible from backend
|
||||
- Check network bandwidth between backend and WebDAV
|
||||
- Browser console may show streaming errors
|
||||
- Try downloading video directly from WebDAV to verify file integrity
|
||||
|
||||
**Migration**:
|
||||
- Existing local videos remain in `backend/downloads/`
|
||||
- New videos go to WebDAV when enabled
|
||||
- To migrate existing videos, manually upload to WebDAV and update database `filePath`
|
||||
- Or delete and re-download videos
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Credentials stored in local database (plaintext)
|
||||
- Backend must have network access to WebDAV server
|
||||
- Use HTTPS for WebDAV URL to encrypt credentials in transit
|
||||
- Consider firewall rules to restrict WebDAV access to backend IP only
|
||||
- WebDAV server should require authentication
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
# VidRip Caddyfile
|
||||
# This configuration serves the frontend and proxies API requests to the backend
|
||||
#
|
||||
# NOTE: This file will be auto-generated by start-production.sh if it doesn't exist.
|
||||
# The script will prompt you for your domain name.
|
||||
#
|
||||
# Manual Usage:
|
||||
# 1. Install Caddy: https://caddyserver.com/docs/install
|
||||
# 2. Update 'your-domain.com' below with your actual domain
|
||||
# 3. Run: caddy run (for testing) or caddy start (background)
|
||||
# 4. Or use systemd service (see DEPLOYMENT.md)
|
||||
#
|
||||
# Features:
|
||||
# - Automatic HTTPS with Let's Encrypt
|
||||
# - Reverse proxy to backend API
|
||||
# - Static file serving for frontend
|
||||
# - Compression enabled
|
||||
# - Security headers
|
||||
|
||||
# Replace with your domain or use :80 for localhost
|
||||
your-domain.com {
|
||||
# Enable compression
|
||||
encode gzip zstd
|
||||
|
||||
# Security headers
|
||||
header {
|
||||
# Enable HSTS (forces HTTPS)
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
# Prevent clickjacking
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
# Prevent MIME type sniffing
|
||||
X-Content-Type-Options "nosniff"
|
||||
# Enable XSS protection
|
||||
X-XSS-Protection "1; mode=block"
|
||||
# Referrer policy
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
# Remove server header for security
|
||||
-Server
|
||||
}
|
||||
|
||||
# API routes - proxy to backend
|
||||
handle /api/* {
|
||||
reverse_proxy localhost:3001 {
|
||||
# Health check
|
||||
health_uri /api/health
|
||||
health_interval 10s
|
||||
health_timeout 5s
|
||||
|
||||
# Headers
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
|
||||
# Serve frontend static files
|
||||
handle {
|
||||
# Root directory is the frontend build output
|
||||
root * /var/www/vidrip
|
||||
|
||||
# Try files first, fall back to index.html for SPA routing
|
||||
try_files {path} /index.html
|
||||
|
||||
# Serve files
|
||||
file_server
|
||||
|
||||
# Cache static assets
|
||||
@static {
|
||||
path *.js *.css *.woff *.woff2 *.ttf *.eot *.ico *.png *.jpg *.jpeg *.gif *.svg *.webp
|
||||
}
|
||||
header @static {
|
||||
Cache-Control "public, max-age=31536000, immutable"
|
||||
}
|
||||
|
||||
# Don't cache index.html
|
||||
@html {
|
||||
path *.html
|
||||
}
|
||||
header @html {
|
||||
Cache-Control "no-cache, no-store, must-revalidate"
|
||||
}
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output file /var/log/caddy/vidrip-access.log {
|
||||
roll_size 100mb
|
||||
roll_keep 10
|
||||
}
|
||||
format json
|
||||
}
|
||||
}
|
||||
|
||||
# Alternative configuration for local development/testing without a domain
|
||||
# Uncomment this and comment out the domain configuration above
|
||||
# :80 {
|
||||
# encode gzip zstd
|
||||
#
|
||||
# handle /api/* {
|
||||
# reverse_proxy localhost:3001
|
||||
# }
|
||||
#
|
||||
# handle {
|
||||
# root * /var/www/vidrip
|
||||
# try_files {path} /index.html
|
||||
# file_server
|
||||
# }
|
||||
#
|
||||
# log {
|
||||
# output stdout
|
||||
# }
|
||||
# }
|
||||
106
DEPLOYMENT.md
106
DEPLOYMENT.md
|
|
@ -10,19 +10,50 @@ The simplest way to run VidRip in production:
|
|||
# Make scripts executable (if not already)
|
||||
chmod +x start-production.sh stop-production.sh
|
||||
|
||||
# Start the service
|
||||
# Start the service (simple mode with nohup)
|
||||
./start-production.sh
|
||||
|
||||
# When prompted, enter your domain name (e.g., vidrip.example.com)
|
||||
# Or press Enter to skip Caddyfile generation
|
||||
|
||||
# Stop the service
|
||||
./stop-production.sh
|
||||
```
|
||||
|
||||
**OR with systemd (recommended for production):**
|
||||
|
||||
```bash
|
||||
# Start with systemd (auto-starts on boot, better process management)
|
||||
sudo ./start-production.sh --systemd
|
||||
|
||||
# Stop systemd service
|
||||
sudo ./stop-production.sh --systemd
|
||||
```
|
||||
|
||||
The `start-production.sh` script will:
|
||||
- Check system requirements (Node.js 18+, yt-dlp)
|
||||
- Install dependencies
|
||||
- Build backend and frontend
|
||||
- Start the backend server in the background
|
||||
- **Ask for your domain and auto-generate Caddyfile** (if not exists)
|
||||
- Deploy frontend to `/var/www/vidrip`
|
||||
- Start the backend server (nohup or systemd)
|
||||
- Create log files in `logs/` directory
|
||||
- Show next steps for Caddy installation
|
||||
|
||||
### Deployment Modes
|
||||
|
||||
**Simple Mode (nohup):**
|
||||
- No sudo required
|
||||
- Uses PID file for process management
|
||||
- Logs to `logs/` directory
|
||||
- Good for testing or simple deployments
|
||||
|
||||
**Systemd Mode (--systemd):**
|
||||
- Requires sudo
|
||||
- Auto-starts on system boot
|
||||
- Better process management and monitoring
|
||||
- Integration with system logging (`journalctl`)
|
||||
- Recommended for production servers
|
||||
|
||||
## Production Deployment (Recommended)
|
||||
|
||||
|
|
@ -106,7 +137,63 @@ sudo systemctl start vidrip
|
|||
sudo systemctl status vidrip
|
||||
```
|
||||
|
||||
### 6. Setup Nginx Reverse Proxy
|
||||
### 6. Setup Web Server (Reverse Proxy)
|
||||
|
||||
Choose one of the following options:
|
||||
|
||||
#### Option A: Caddy (Recommended - Easiest with automatic HTTPS)
|
||||
|
||||
**Install Caddy:**
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
sudo apt update
|
||||
sudo apt install caddy
|
||||
```
|
||||
|
||||
**Configure Caddyfile:**
|
||||
|
||||
If you ran `start-production.sh` and entered your domain, a Caddyfile was auto-generated for you!
|
||||
|
||||
Otherwise, edit the included template:
|
||||
|
||||
```bash
|
||||
# Edit Caddyfile
|
||||
nano Caddyfile
|
||||
|
||||
# Change line 21:
|
||||
# FROM: your-domain.com {
|
||||
# TO: yourdomain.com {
|
||||
```
|
||||
|
||||
**Deploy Caddyfile:**
|
||||
|
||||
```bash
|
||||
# Copy to standard location
|
||||
sudo cp Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
# Validate configuration
|
||||
sudo caddy validate --config /etc/caddy/Caddyfile
|
||||
|
||||
# Enable and start Caddy
|
||||
sudo systemctl enable caddy
|
||||
sudo systemctl start caddy
|
||||
|
||||
# Check status
|
||||
sudo systemctl status caddy
|
||||
```
|
||||
|
||||
Caddy will automatically obtain and renew SSL certificates from Let's Encrypt!
|
||||
|
||||
**View logs:**
|
||||
```bash
|
||||
sudo journalctl -u caddy -f
|
||||
```
|
||||
|
||||
#### Option B: Nginx
|
||||
|
||||
Create `/etc/nginx/sites-available/vidrip`:
|
||||
|
||||
|
|
@ -117,7 +204,7 @@ server {
|
|||
|
||||
# Serve frontend static files
|
||||
location / {
|
||||
root /var/www/vidrip/frontend/dist;
|
||||
root /var/www/vidrip;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
|
|
@ -134,11 +221,8 @@ server {
|
|||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Serve downloaded video files
|
||||
location /downloads/ {
|
||||
alias /var/www/vidrip/backend/downloads/;
|
||||
add_header Content-Type video/mp4;
|
||||
}
|
||||
# Note: Video files are now served via /api/videos/:id/stream
|
||||
# This handles both local and WebDAV storage automatically
|
||||
|
||||
# Increase upload size if needed
|
||||
client_max_body_size 100M;
|
||||
|
|
@ -153,13 +237,15 @@ sudo nginx -t
|
|||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### 7. Setup SSL with Let's Encrypt (Recommended)
|
||||
**Setup SSL with Let's Encrypt:**
|
||||
|
||||
```bash
|
||||
sudo apt-get install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
```
|
||||
|
||||
Note: If using Caddy, SSL is automatic - skip this step!
|
||||
|
||||
## Systemd Service Management
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@
|
|||
"cors": "^2.8.5",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"dotenv": "^16.3.1"
|
||||
"dotenv": "^16.3.1",
|
||||
"webdav": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
|
|
|
|||
|
|
@ -89,7 +89,12 @@ export function initDatabase() {
|
|||
intervalMinutes: '180', // 180 minutes = 3 hours
|
||||
varianceMinutes: '30',
|
||||
maxConcurrentDownloads: '1',
|
||||
enabled: 'true'
|
||||
enabled: 'true',
|
||||
webdavEnabled: 'false',
|
||||
webdavUrl: '',
|
||||
webdavUsername: '',
|
||||
webdavPassword: '',
|
||||
webdavPath: '/vidrip/'
|
||||
};
|
||||
|
||||
const insertConfig = db.prepare(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Router } from 'express';
|
||||
import { configOperations } from '../db/database';
|
||||
import { restartScheduler, getSchedulerStatus } from '../services/scheduler';
|
||||
import { testConnection } from '../services/webdav';
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -46,4 +47,32 @@ router.get('/scheduler/status', (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Test WebDAV connection
|
||||
router.post('/webdav/test', async (req, res) => {
|
||||
try {
|
||||
const { url, username, password, path } = req.body;
|
||||
|
||||
if (!url || !username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'URL, username, and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await testConnection({
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
path: path || '/vidrip/'
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Failed to test connection'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Router } from 'express';
|
||||
import { videoOperations } from '../db/database';
|
||||
import { getVideoInfo, downloadVideo } from '../services/ytdlp';
|
||||
import { isWebDAVEnabled, uploadVideo as uploadToWebDAV, deleteVideo as deleteFromWebDAV, streamVideo as streamFromWebDAV, getVideoStats } from '../services/webdav';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ router.post('/', async (req, res) => {
|
|||
});
|
||||
|
||||
// Delete video
|
||||
router.delete('/:id', (req, res) => {
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
|
|
@ -99,9 +100,22 @@ router.delete('/:id', (req, res) => {
|
|||
return res.status(404).json({ error: 'Video not found' });
|
||||
}
|
||||
|
||||
// Delete file if it exists
|
||||
if (video.filePath && fs.existsSync(video.filePath)) {
|
||||
fs.unlinkSync(video.filePath);
|
||||
// Delete file based on storage type
|
||||
if (video.filePath) {
|
||||
if (video.filePath.startsWith('webdav://')) {
|
||||
// Delete from WebDAV
|
||||
try {
|
||||
await deleteFromWebDAV(video.videoId);
|
||||
console.log(`Deleted ${video.videoId} from WebDAV`);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete from WebDAV:', error);
|
||||
// Continue with database deletion even if WebDAV delete fails
|
||||
}
|
||||
} else if (fs.existsSync(video.filePath)) {
|
||||
// Delete local file
|
||||
fs.unlinkSync(video.filePath);
|
||||
console.log(`Deleted local file: ${video.filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
videoOperations.delete(id);
|
||||
|
|
@ -164,7 +178,37 @@ router.post('/:id/download', async (req, res) => {
|
|||
videoOperations.updateStatus(id, 'downloading');
|
||||
|
||||
const result = await downloadVideo(video.videoId, video.url);
|
||||
videoOperations.markCompleted(id, result.filePath, result.fileSize);
|
||||
|
||||
// Check if WebDAV is enabled
|
||||
if (isWebDAVEnabled()) {
|
||||
console.log(`Uploading ${video.videoId} to WebDAV...`);
|
||||
try {
|
||||
await uploadToWebDAV(result.filePath, video.videoId);
|
||||
console.log(`Successfully uploaded ${video.videoId} to WebDAV`);
|
||||
|
||||
// Delete local file after successful upload
|
||||
if (fs.existsSync(result.filePath)) {
|
||||
fs.unlinkSync(result.filePath);
|
||||
console.log(`Deleted local file: ${result.filePath}`);
|
||||
}
|
||||
|
||||
// Mark as completed with WebDAV path
|
||||
videoOperations.markCompleted(id, `webdav://${video.videoId}.mp4`, result.fileSize);
|
||||
} catch (uploadError) {
|
||||
console.error('WebDAV upload failed:', uploadError);
|
||||
// Delete local file and mark as failed
|
||||
if (fs.existsSync(result.filePath)) {
|
||||
fs.unlinkSync(result.filePath);
|
||||
}
|
||||
throw new Error(
|
||||
`WebDAV upload failed: ${uploadError instanceof Error ? uploadError.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Local storage mode
|
||||
videoOperations.markCompleted(id, result.filePath, result.fileSize);
|
||||
}
|
||||
|
||||
console.log(`Immediate download completed: ${video.title}`);
|
||||
} catch (error) {
|
||||
console.error('Immediate download error:', error);
|
||||
|
|
@ -186,4 +230,88 @@ router.post('/:id/download', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Stream video (WebDAV proxy or local file)
|
||||
router.get('/:id/stream', async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const video = videoOperations.getById(id);
|
||||
|
||||
if (!video) {
|
||||
return res.status(404).json({ error: 'Video not found' });
|
||||
}
|
||||
|
||||
if (video.status !== 'completed') {
|
||||
return res.status(400).json({ error: 'Video not yet downloaded' });
|
||||
}
|
||||
|
||||
// Check if video is stored on WebDAV (filePath starts with webdav://)
|
||||
if (video.filePath && video.filePath.startsWith('webdav://')) {
|
||||
// Stream from WebDAV
|
||||
try {
|
||||
const stream = await streamFromWebDAV(video.videoId);
|
||||
const stats = await getVideoStats(video.videoId);
|
||||
|
||||
// Set headers
|
||||
res.setHeader('Content-Type', 'video/mp4');
|
||||
if (stats && stats.size) {
|
||||
res.setHeader('Content-Length', stats.size.toString());
|
||||
}
|
||||
res.setHeader('Accept-Ranges', 'bytes');
|
||||
|
||||
// Pipe stream to response
|
||||
stream.pipe(res);
|
||||
|
||||
stream.on('error', (error) => {
|
||||
console.error('Stream error:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: 'Failed to stream video' });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to stream from WebDAV:', error);
|
||||
res.status(500).json({ error: 'Failed to stream video from WebDAV' });
|
||||
}
|
||||
} else if (video.filePath && fs.existsSync(video.filePath)) {
|
||||
// Stream from local file
|
||||
const stat = fs.statSync(video.filePath);
|
||||
const fileSize = stat.size;
|
||||
const range = req.headers.range;
|
||||
|
||||
if (range) {
|
||||
// Handle range requests for seeking
|
||||
const parts = range.replace(/bytes=/, '').split('-');
|
||||
const start = parseInt(parts[0], 10);
|
||||
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
||||
const chunkSize = (end - start) + 1;
|
||||
const file = fs.createReadStream(video.filePath, { start, end });
|
||||
|
||||
res.writeHead(206, {
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': chunkSize,
|
||||
'Content-Type': 'video/mp4',
|
||||
});
|
||||
|
||||
file.pipe(res);
|
||||
} else {
|
||||
// No range request - stream entire file
|
||||
res.writeHead(200, {
|
||||
'Content-Length': fileSize,
|
||||
'Content-Type': 'video/mp4',
|
||||
'Accept-Ranges': 'bytes',
|
||||
});
|
||||
|
||||
fs.createReadStream(video.filePath).pipe(res);
|
||||
}
|
||||
} else {
|
||||
res.status(404).json({ error: 'Video file not found' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Stream error:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: 'Failed to stream video' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
} from "../db/database";
|
||||
import { downloadVideo, getChannelVideos } from "./ytdlp";
|
||||
import { DownloadProgress } from "../types";
|
||||
import { isWebDAVEnabled, uploadVideo as uploadToWebDAV } from "./webdav";
|
||||
import fs from "fs";
|
||||
|
||||
let schedulerTask: cron.ScheduledTask | null = null;
|
||||
let isDownloading = false;
|
||||
|
|
@ -58,7 +60,40 @@ async function downloadNextVideo() {
|
|||
},
|
||||
});
|
||||
|
||||
videoOperations.markCompleted(video.id, result.filePath, result.fileSize);
|
||||
// Check if WebDAV is enabled
|
||||
if (isWebDAVEnabled()) {
|
||||
console.log(`Uploading ${video.videoId} to WebDAV...`);
|
||||
try {
|
||||
await uploadToWebDAV(result.filePath, video.videoId);
|
||||
console.log(`Successfully uploaded ${video.videoId} to WebDAV`);
|
||||
|
||||
// Delete local file after successful upload
|
||||
if (fs.existsSync(result.filePath)) {
|
||||
fs.unlinkSync(result.filePath);
|
||||
console.log(`Deleted local file: ${result.filePath}`);
|
||||
}
|
||||
|
||||
// Mark as completed with WebDAV path
|
||||
videoOperations.markCompleted(
|
||||
video.id,
|
||||
`webdav://${video.videoId}.mp4`,
|
||||
result.fileSize,
|
||||
);
|
||||
} catch (uploadError) {
|
||||
console.error("WebDAV upload failed:", uploadError);
|
||||
// Delete local file and mark as failed
|
||||
if (fs.existsSync(result.filePath)) {
|
||||
fs.unlinkSync(result.filePath);
|
||||
}
|
||||
throw new Error(
|
||||
`WebDAV upload failed: ${uploadError instanceof Error ? uploadError.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Local storage mode
|
||||
videoOperations.markCompleted(video.id, result.filePath, result.fileSize);
|
||||
}
|
||||
|
||||
console.log(`Download completed: ${video.title}`);
|
||||
currentProgress = null;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,265 @@
|
|||
import { createClient, WebDAVClient, FileStat } from 'webdav';
|
||||
import { createReadStream, createWriteStream, statSync } from 'fs';
|
||||
import { Readable } from 'stream';
|
||||
import { configOperations } from '../db/database';
|
||||
|
||||
let webdavClient: WebDAVClient | null = null;
|
||||
|
||||
export interface WebDAVConfig {
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebDAV configuration from database
|
||||
*/
|
||||
function getWebDAVConfig(): WebDAVConfig | null {
|
||||
const config = configOperations.getAll();
|
||||
|
||||
if (config.webdavEnabled !== 'true') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!config.webdavUrl || !config.webdavUsername || !config.webdavPassword) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
url: config.webdavUrl,
|
||||
username: config.webdavUsername,
|
||||
password: config.webdavPassword,
|
||||
path: config.webdavPath || '/vidrip/'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create authenticated WebDAV client from config
|
||||
*/
|
||||
export function createWebDAVClient(config?: WebDAVConfig): WebDAVClient | null {
|
||||
const webdavConfig = config || getWebDAVConfig();
|
||||
|
||||
if (!webdavConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return createClient(webdavConfig.url, {
|
||||
username: webdavConfig.username,
|
||||
password: webdavConfig.password
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create WebDAV client:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test WebDAV connection and permissions
|
||||
*/
|
||||
export async function testConnection(config: WebDAVConfig): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const client = createWebDAVClient(config);
|
||||
|
||||
if (!client) {
|
||||
return { success: false, message: 'Failed to create WebDAV client' };
|
||||
}
|
||||
|
||||
// Ensure path ends with /
|
||||
const basePath = config.path.endsWith('/') ? config.path : `${config.path}/`;
|
||||
|
||||
// Test connection by checking if base path exists
|
||||
const exists = await client.exists(basePath);
|
||||
|
||||
if (!exists) {
|
||||
// Try to create the directory
|
||||
try {
|
||||
await client.createDirectory(basePath, { recursive: true });
|
||||
return { success: true, message: 'Connected successfully and created directory' };
|
||||
} catch (createError) {
|
||||
return { success: false, message: `Directory doesn't exist and cannot be created: ${createError}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get directory contents to verify read permissions
|
||||
await client.getDirectoryContents(basePath);
|
||||
|
||||
return { success: true, message: 'Connected successfully' };
|
||||
} catch (error) {
|
||||
console.error('WebDAV connection test failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Connection failed'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full WebDAV path for a video
|
||||
*/
|
||||
function getVideoPath(videoId: string): string {
|
||||
const config = getWebDAVConfig();
|
||||
if (!config) {
|
||||
throw new Error('WebDAV not configured');
|
||||
}
|
||||
|
||||
const basePath = config.path.endsWith('/') ? config.path : `${config.path}/`;
|
||||
return `${basePath}${videoId}.mp4`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload video file to WebDAV
|
||||
*/
|
||||
export async function uploadVideo(localPath: string, videoId: string): Promise<void> {
|
||||
const config = getWebDAVConfig();
|
||||
|
||||
if (!config) {
|
||||
throw new Error('WebDAV not configured');
|
||||
}
|
||||
|
||||
const client = createWebDAVClient(config);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Failed to create WebDAV client');
|
||||
}
|
||||
|
||||
try {
|
||||
const remotePath = getVideoPath(videoId);
|
||||
const basePath = config.path.endsWith('/') ? config.path : `${config.path}/`;
|
||||
|
||||
// Ensure directory exists
|
||||
const dirExists = await client.exists(basePath);
|
||||
if (!dirExists) {
|
||||
await client.createDirectory(basePath, { recursive: true });
|
||||
}
|
||||
|
||||
// Upload file
|
||||
const fileStream = createReadStream(localPath);
|
||||
await client.putFileContents(remotePath, fileStream, {
|
||||
overwrite: true
|
||||
});
|
||||
|
||||
console.log(`Successfully uploaded ${videoId}.mp4 to WebDAV`);
|
||||
} catch (error) {
|
||||
console.error('Failed to upload to WebDAV:', error);
|
||||
throw new Error(`WebDAV upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete video from WebDAV server
|
||||
*/
|
||||
export async function deleteVideo(videoId: string): Promise<void> {
|
||||
const config = getWebDAVConfig();
|
||||
|
||||
if (!config) {
|
||||
// If WebDAV not configured, silently return (nothing to delete)
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createWebDAVClient(config);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Failed to create WebDAV client');
|
||||
}
|
||||
|
||||
try {
|
||||
const remotePath = getVideoPath(videoId);
|
||||
|
||||
const exists = await client.exists(remotePath);
|
||||
if (exists) {
|
||||
await client.deleteFile(remotePath);
|
||||
console.log(`Successfully deleted ${videoId}.mp4 from WebDAV`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete from WebDAV:', error);
|
||||
throw new Error(`WebDAV delete failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get readable stream from WebDAV for proxying
|
||||
*/
|
||||
export async function streamVideo(videoId: string): Promise<Readable> {
|
||||
const config = getWebDAVConfig();
|
||||
|
||||
if (!config) {
|
||||
throw new Error('WebDAV not configured');
|
||||
}
|
||||
|
||||
const client = createWebDAVClient(config);
|
||||
|
||||
if (!client) {
|
||||
throw new Error('Failed to create WebDAV client');
|
||||
}
|
||||
|
||||
try {
|
||||
const remotePath = getVideoPath(videoId);
|
||||
|
||||
const stream = client.createReadStream(remotePath);
|
||||
return stream as Readable;
|
||||
} catch (error) {
|
||||
console.error('Failed to stream from WebDAV:', error);
|
||||
throw new Error(`WebDAV stream failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if video exists on WebDAV
|
||||
*/
|
||||
export async function getVideoExists(videoId: string): Promise<boolean> {
|
||||
const config = getWebDAVConfig();
|
||||
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const client = createWebDAVClient(config);
|
||||
|
||||
if (!client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const remotePath = getVideoPath(videoId);
|
||||
return await client.exists(remotePath);
|
||||
} catch (error) {
|
||||
console.error('Failed to check video existence on WebDAV:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get video file stats from WebDAV
|
||||
*/
|
||||
export async function getVideoStats(videoId: string): Promise<FileStat | null> {
|
||||
const config = getWebDAVConfig();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const client = createWebDAVClient(config);
|
||||
|
||||
if (!client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const remotePath = getVideoPath(videoId);
|
||||
const stat = await client.stat(remotePath) as FileStat;
|
||||
return stat;
|
||||
} catch (error) {
|
||||
console.error('Failed to get video stats from WebDAV:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebDAV is enabled and configured
|
||||
*/
|
||||
export function isWebDAVEnabled(): boolean {
|
||||
const config = getWebDAVConfig();
|
||||
return config !== null;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { spawn } from 'child_process';
|
|||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const DOWNLOADS_DIR = path.join(__dirname, '../../downloads');
|
||||
const DOWNLOADS_DIR = '/var/www/vidrip/downloads';
|
||||
|
||||
// Ensure downloads directory exists
|
||||
if (!fs.existsSync(DOWNLOADS_DIR)) {
|
||||
|
|
@ -15,8 +15,8 @@ const COOKIES_PATH = path.join(__dirname, '../../cookies.txt');
|
|||
// Common args to avoid 403 errors
|
||||
const getCommonArgs = () => {
|
||||
const args = [
|
||||
'--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'--referer', 'https://www.youtube.com/',
|
||||
'--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.183 Safari/537.36',
|
||||
'--proxy', 'ny_premium:pJTg94JCukVjz2b@107.152.43.179:3128',
|
||||
'--extractor-retries', '5',
|
||||
'--retries', '10'
|
||||
];
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ function SettingsPage() {
|
|||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [testingWebDAV, setTestingWebDAV] = useState(false);
|
||||
const [webdavTestResult, setWebdavTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
|
|
@ -61,6 +63,31 @@ function SettingsPage() {
|
|||
const handleChange = (key: keyof Config, value: string) => {
|
||||
if (!config) return;
|
||||
setConfig({ ...config, [key]: value });
|
||||
// Clear test result when config changes
|
||||
setWebdavTestResult(null);
|
||||
};
|
||||
|
||||
const handleTestWebDAV = async () => {
|
||||
if (!config) return;
|
||||
|
||||
try {
|
||||
setTestingWebDAV(true);
|
||||
setWebdavTestResult(null);
|
||||
const result = await configAPI.testWebDAVConnection({
|
||||
url: config.webdavUrl,
|
||||
username: config.webdavUsername,
|
||||
password: config.webdavPassword,
|
||||
path: config.webdavPath || '/vidrip/'
|
||||
});
|
||||
setWebdavTestResult(result);
|
||||
} catch (err) {
|
||||
setWebdavTestResult({
|
||||
success: false,
|
||||
message: err instanceof Error ? err.message : 'Test failed'
|
||||
});
|
||||
} finally {
|
||||
setTestingWebDAV(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -224,6 +251,117 @@ function SettingsPage() {
|
|||
Number of videos to download simultaneously (recommended: 1)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">WebDAV Storage</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.webdavEnabled === 'true'}
|
||||
onChange={(e) => handleChange('webdavEnabled', e.target.checked ? 'true' : 'false')}
|
||||
className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700"
|
||||
/>
|
||||
<span className="ml-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
Enable WebDAV Storage
|
||||
</span>
|
||||
</label>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Store videos on a WebDAV server instead of local storage
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{config.webdavEnabled === 'true' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
WebDAV Server URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.webdavUrl}
|
||||
onChange={(e) => handleChange('webdavUrl', e.target.value)}
|
||||
placeholder="https://webdav.example.com"
|
||||
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.webdavUsername}
|
||||
onChange={(e) => handleChange('webdavUsername', e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={config.webdavPassword}
|
||||
onChange={(e) => handleChange('webdavPassword', e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Directory Path
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.webdavPath}
|
||||
onChange={(e) => handleChange('webdavPath', e.target.value)}
|
||||
placeholder="/vidrip/"
|
||||
className="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Directory path on the WebDAV server where videos will be stored
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTestWebDAV}
|
||||
disabled={testingWebDAV || !config.webdavUrl || !config.webdavUsername || !config.webdavPassword}
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{testingWebDAV ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
|
||||
{webdavTestResult && (
|
||||
<div className={`mt-3 p-3 rounded ${
|
||||
webdavTestResult.success
|
||||
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-400'
|
||||
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400'
|
||||
}`}>
|
||||
{webdavTestResult.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-300">Important Notes</h4>
|
||||
<ul className="mt-2 text-sm text-blue-700 dark:text-blue-400 list-disc list-inside space-y-1">
|
||||
<li>Videos will be uploaded to WebDAV after downloading</li>
|
||||
<li>Local copies are deleted after successful upload</li>
|
||||
<li>Backend must have network access to the WebDAV server</li>
|
||||
<li>Credentials are stored in the local database</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end">
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ function VideoPlayerPage() {
|
|||
<video
|
||||
controls
|
||||
className="w-full h-full"
|
||||
src={`/downloads/${video.videoId}.mp4`}
|
||||
src={`/api/videos/${video.id}/stream`}
|
||||
>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
|
|
|||
|
|
@ -135,4 +135,10 @@ export const configAPI = {
|
|||
|
||||
getSchedulerStatus: () =>
|
||||
fetchJSON<SchedulerStatus>(`${API_BASE}/config/scheduler/status`),
|
||||
|
||||
testWebDAVConnection: (config: { url: string; username: string; password: string; path: string }) =>
|
||||
fetchJSON<{ success: boolean; message: string }>(`${API_BASE}/config/webdav/test`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ export interface Config {
|
|||
varianceMinutes: string;
|
||||
maxConcurrentDownloads: string;
|
||||
enabled: string;
|
||||
webdavEnabled: string;
|
||||
webdavUrl: string;
|
||||
webdavUsername: string;
|
||||
webdavPassword: string;
|
||||
webdavPath: string;
|
||||
}
|
||||
|
||||
export interface SchedulerStatus {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,30 @@
|
|||
set -e # Exit on error
|
||||
set -u # Exit on undefined variable
|
||||
|
||||
# Parse command line arguments
|
||||
USE_SYSTEMD=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--systemd)
|
||||
USE_SYSTEMD=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --systemd Use systemd service instead of nohup (requires sudo)"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Color codes for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
|
|
@ -19,7 +43,11 @@ BLUE='\033[0;34m'
|
|||
NC='\033[0m' # No Color
|
||||
|
||||
# Script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WEB_ROOT="/var/www/vidrip"
|
||||
SCRIPT_DIR=$(pwd)
|
||||
echo -e "$SCRIPT_DIR"
|
||||
|
||||
rm -rf "$WEB_ROOT/*"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Configuration
|
||||
|
|
@ -191,51 +219,259 @@ log_success "Frontend built successfully"
|
|||
cd "$SCRIPT_DIR"
|
||||
echo ""
|
||||
|
||||
################################################################################
|
||||
# Generate Caddyfile (if needed)
|
||||
################################################################################
|
||||
|
||||
if [ ! -f "$SCRIPT_DIR/Caddyfile" ]; then
|
||||
log_info "Caddyfile not found. Let's create one!"
|
||||
echo ""
|
||||
|
||||
# Ask for domain
|
||||
read -p "Enter your domain name (or press Enter to skip): " DOMAIN_INPUT
|
||||
|
||||
if [ -n "$DOMAIN_INPUT" ]; then
|
||||
log_info "Generating Caddyfile for domain: $DOMAIN_INPUT"
|
||||
|
||||
cat > "$SCRIPT_DIR/Caddyfile" << EOF
|
||||
# VidRip Caddyfile
|
||||
# Auto-generated configuration for ${DOMAIN_INPUT}
|
||||
|
||||
${DOMAIN_INPUT} {
|
||||
# Enable compression
|
||||
encode gzip zstd
|
||||
|
||||
# Security headers
|
||||
header {
|
||||
# Enable HSTS (forces HTTPS)
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
# Prevent clickjacking
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
# Prevent MIME type sniffing
|
||||
X-Content-Type-Options "nosniff"
|
||||
# Enable XSS protection
|
||||
X-XSS-Protection "1; mode=block"
|
||||
# Referrer policy
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
# Remove server header for security
|
||||
-Server
|
||||
}
|
||||
|
||||
# API routes - proxy to backend
|
||||
handle /api/* {
|
||||
reverse_proxy localhost:3001 {
|
||||
# Health check
|
||||
health_uri /api/health
|
||||
health_interval 10s
|
||||
health_timeout 5s
|
||||
|
||||
# Headers
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
|
||||
# Serve frontend static files
|
||||
handle {
|
||||
# Root directory is the frontend build output
|
||||
root * ${WEB_ROOT}
|
||||
|
||||
# Try files first, fall back to index.html for SPA routing
|
||||
try_files {path} /index.html
|
||||
|
||||
# Serve files
|
||||
file_server
|
||||
|
||||
# Cache static assets
|
||||
@static {
|
||||
path *.js *.css *.woff *.woff2 *.ttf *.eot *.ico *.png *.jpg *.jpeg *.gif *.svg *.webp
|
||||
}
|
||||
header @static {
|
||||
Cache-Control "public, max-age=31536000, immutable"
|
||||
}
|
||||
|
||||
# Don't cache index.html
|
||||
@html {
|
||||
path *.html
|
||||
}
|
||||
header @html {
|
||||
Cache-Control "no-cache, no-store, must-revalidate"
|
||||
}
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output file /var/log/caddy/vidrip-access.log {
|
||||
roll_size 100mb
|
||||
roll_keep 10
|
||||
}
|
||||
format json
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
log_success "Caddyfile created successfully!"
|
||||
log_info "You can edit it later at: $SCRIPT_DIR/Caddyfile"
|
||||
else
|
||||
log_info "Skipping Caddyfile generation"
|
||||
log_info "You can create it manually later or use nginx/Apache"
|
||||
fi
|
||||
echo ""
|
||||
else
|
||||
log_info "Caddyfile already exists, skipping generation"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# Deploy Frontend to Web Root
|
||||
################################################################################
|
||||
|
||||
log_info "Deploying frontend to web root..."
|
||||
|
||||
# Check if we have permissions to write to /var/www
|
||||
if [ -w "/var/www" ]; then
|
||||
# Create web root directory
|
||||
sudo mkdir -p "$WEB_ROOT"
|
||||
|
||||
# Copy frontend build to web root
|
||||
log_info "Copying frontend build to $WEB_ROOT..."
|
||||
sudo rm -rf "$WEB_ROOT"/*
|
||||
sudo cp -r "$FRONTEND_DIR/dist/"* "$WEB_ROOT/"
|
||||
|
||||
# Set proper permissions
|
||||
sudo chown -R www-data:www-data "$WEB_ROOT" 2>/dev/null || sudo chown -R $USER:$USER "$WEB_ROOT"
|
||||
sudo chmod -R 755 "$WEB_ROOT"
|
||||
|
||||
log_success "Frontend deployed to $WEB_ROOT"
|
||||
else
|
||||
log_warning "No write permission to /var/www"
|
||||
log_warning "Run with sudo to deploy frontend, or manually copy:"
|
||||
log_warning " sudo mkdir -p $WEB_ROOT"
|
||||
log_warning " sudo cp -r $FRONTEND_DIR/dist/* $WEB_ROOT/"
|
||||
log_warning " sudo chown -R www-data:www-data $WEB_ROOT"
|
||||
log_warning " sudo chmod -R 755 $WEB_ROOT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
################################################################################
|
||||
# Start Backend Server
|
||||
################################################################################
|
||||
|
||||
log_info "Starting VidRip backend server..."
|
||||
|
||||
# Setup environment
|
||||
export NODE_ENV=production
|
||||
if [ "$USE_SYSTEMD" = true ]; then
|
||||
# Systemd mode
|
||||
log_info "Using systemd service..."
|
||||
|
||||
# Start the backend server
|
||||
cd "$BACKEND_DIR"
|
||||
# Check if running as root or with sudo
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
log_error "Systemd mode requires root privileges"
|
||||
log_error "Please run with: sudo $0 --systemd"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run in background with output redirected to log files
|
||||
nohup node dist/server.js > "$LOG_FILE" 2> "$ERROR_LOG_FILE" &
|
||||
SERVER_PID=$!
|
||||
# Check if service file exists
|
||||
SERVICE_FILE="$SCRIPT_DIR/vidrip-backend.service"
|
||||
if [ ! -f "$SERVICE_FILE" ]; then
|
||||
log_error "Service file not found: $SERVICE_FILE"
|
||||
log_error "Please ensure vidrip-backend.service exists in the project directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save PID
|
||||
echo "$SERVER_PID" > "$PID_FILE"
|
||||
# Update service file with correct paths
|
||||
TEMP_SERVICE="/tmp/vidrip-backend.service"
|
||||
sed -e "s|/opt/vidrip|$SCRIPT_DIR|g" \
|
||||
-e "s|User=vidrip|User=$SUDO_USER|g" \
|
||||
-e "s|Group=vidrip|Group=$SUDO_USER|g" \
|
||||
-e "s|/usr/bin/node|$(which node)|g" \
|
||||
"$SERVICE_FILE" > "$TEMP_SERVICE"
|
||||
|
||||
# Wait a moment and check if process is still running
|
||||
sleep 2
|
||||
if ! ps -p "$SERVER_PID" > /dev/null 2>&1; then
|
||||
log_error "Server failed to start"
|
||||
log_error "Check logs at:"
|
||||
log_error " - $LOG_FILE"
|
||||
log_error " - $ERROR_LOG_FILE"
|
||||
rm -f "$PID_FILE"
|
||||
exit 1
|
||||
fi
|
||||
# Install service file
|
||||
cp "$TEMP_SERVICE" /etc/systemd/system/vidrip-backend.service
|
||||
rm "$TEMP_SERVICE"
|
||||
|
||||
log_success "VidRip backend started successfully!"
|
||||
echo ""
|
||||
log_info "Process ID: $SERVER_PID"
|
||||
log_info "PID file: $PID_FILE"
|
||||
log_info "Log file: $LOG_FILE"
|
||||
log_info "Error log: $ERROR_LOG_FILE"
|
||||
echo ""
|
||||
log_info "Service file installed to /etc/systemd/system/vidrip-backend.service"
|
||||
|
||||
# Reload systemd
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable and start service
|
||||
systemctl enable vidrip-backend
|
||||
systemctl restart vidrip-backend
|
||||
|
||||
# Wait and check status
|
||||
sleep 2
|
||||
if systemctl is-active --quiet vidrip-backend; then
|
||||
log_success "VidRip backend service started successfully!"
|
||||
echo ""
|
||||
systemctl status vidrip-backend --no-pager -l
|
||||
else
|
||||
log_error "Service failed to start"
|
||||
log_error "Check status with: sudo systemctl status vidrip-backend"
|
||||
log_error "View logs with: sudo journalctl -u vidrip-backend -n 50"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for port in logs (default 3001)
|
||||
sleep 1
|
||||
if grep -q "Server running" "$LOG_FILE" 2>/dev/null; then
|
||||
PORT_INFO=$(grep "Server running" "$LOG_FILE" | tail -1)
|
||||
log_success "$PORT_INFO"
|
||||
else
|
||||
log_info "Server should be running on port 3001 (or PORT from .env)"
|
||||
# Traditional nohup mode
|
||||
log_info "Using nohup mode..."
|
||||
|
||||
# Check for existing process
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
OLD_PID=$(cat "$PID_FILE")
|
||||
if ps -p "$OLD_PID" > /dev/null 2>&1; then
|
||||
log_error "VidRip is already running (PID: $OLD_PID)"
|
||||
log_info "Stop it first with: ./stop-production.sh"
|
||||
exit 1
|
||||
else
|
||||
log_warning "Stale PID file found, removing..."
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Setup environment
|
||||
export NODE_ENV=production
|
||||
|
||||
# Start the backend server
|
||||
cd "$BACKEND_DIR"
|
||||
|
||||
# Run in background with output redirected to log files
|
||||
nohup node dist/server.js > "$LOG_FILE" 2> "$ERROR_LOG_FILE" &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Save PID
|
||||
echo "$SERVER_PID" > "$PID_FILE"
|
||||
|
||||
# Wait a moment and check if process is still running
|
||||
sleep 2
|
||||
if ! ps -p "$SERVER_PID" > /dev/null 2>&1; then
|
||||
log_error "Server failed to start"
|
||||
log_error "Check logs at:"
|
||||
log_error " - $LOG_FILE"
|
||||
log_error " - $ERROR_LOG_FILE"
|
||||
rm -f "$PID_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "VidRip backend started successfully!"
|
||||
echo ""
|
||||
log_info "Process ID: $SERVER_PID"
|
||||
log_info "PID file: $PID_FILE"
|
||||
log_info "Log file: $LOG_FILE"
|
||||
log_info "Error log: $ERROR_LOG_FILE"
|
||||
echo ""
|
||||
|
||||
# Check for port in logs (default 3001)
|
||||
sleep 1
|
||||
if grep -q "Server running" "$LOG_FILE" 2>/dev/null; then
|
||||
PORT_INFO=$(grep "Server running" "$LOG_FILE" | tail -1)
|
||||
log_success "$PORT_INFO"
|
||||
else
|
||||
log_info "Server should be running on port 3001 (or PORT from .env)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
|
@ -243,19 +479,73 @@ log_info "==================================================================="
|
|||
log_info "VidRip is now running in production mode"
|
||||
log_info "==================================================================="
|
||||
echo ""
|
||||
log_info "To stop the service:"
|
||||
log_info " ./stop-production.sh"
|
||||
log_info "Backend Server:"
|
||||
log_info " Running on port 3001 (or PORT from .env)"
|
||||
if [ "$USE_SYSTEMD" = true ]; then
|
||||
log_info " Mode: systemd service"
|
||||
log_info " Service: vidrip-backend"
|
||||
else
|
||||
log_info " Mode: nohup background process"
|
||||
log_info " PID: $SERVER_PID"
|
||||
fi
|
||||
echo ""
|
||||
log_info "To view logs:"
|
||||
log_info " tail -f $LOG_FILE"
|
||||
log_info "Frontend:"
|
||||
log_info " Deployed to: $WEB_ROOT"
|
||||
echo ""
|
||||
log_info "To view errors:"
|
||||
log_info " tail -f $ERROR_LOG_FILE"
|
||||
|
||||
if [ "$USE_SYSTEMD" = true ]; then
|
||||
log_info "Service Management:"
|
||||
log_info " Stop: sudo systemctl stop vidrip-backend"
|
||||
log_info " Restart: sudo systemctl restart vidrip-backend"
|
||||
log_info " Status: sudo systemctl status vidrip-backend"
|
||||
log_info " Disable: sudo systemctl disable vidrip-backend"
|
||||
echo ""
|
||||
log_info "View Logs:"
|
||||
log_info " sudo journalctl -u vidrip-backend -f"
|
||||
log_info " tail -f $LOG_FILE"
|
||||
else
|
||||
log_info "To stop the backend service:"
|
||||
log_info " ./stop-production.sh"
|
||||
echo ""
|
||||
log_info "To view logs:"
|
||||
log_info " tail -f $LOG_FILE"
|
||||
echo ""
|
||||
log_info "To view errors:"
|
||||
log_info " tail -f $ERROR_LOG_FILE"
|
||||
fi
|
||||
echo ""
|
||||
log_warning "IMPORTANT: In production, you'll need to:"
|
||||
log_warning " 1. Serve the frontend build (frontend/dist) via nginx/Apache"
|
||||
log_warning " 2. Setup reverse proxy from frontend to backend API"
|
||||
log_warning " 3. Configure proper firewall rules"
|
||||
if [ -f "$SCRIPT_DIR/Caddyfile" ]; then
|
||||
log_warning "NEXT STEPS - Caddy Setup:"
|
||||
log_warning " 1. Install Caddy: https://caddyserver.com/docs/install"
|
||||
log_warning " Quick install (Ubuntu/Debian):"
|
||||
log_warning " sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https"
|
||||
log_warning " curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg"
|
||||
log_warning " curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list"
|
||||
log_warning " sudo apt update && sudo apt install caddy"
|
||||
echo ""
|
||||
log_warning " 2. Copy Caddyfile to Caddy config directory:"
|
||||
log_warning " sudo cp $SCRIPT_DIR/Caddyfile /etc/caddy/Caddyfile"
|
||||
echo ""
|
||||
log_warning " 3. Start Caddy:"
|
||||
log_warning " sudo systemctl enable caddy"
|
||||
log_warning " sudo systemctl start caddy"
|
||||
echo ""
|
||||
log_warning " 4. Configure firewall to allow ports 80/443:"
|
||||
log_warning " sudo ufw allow 80/tcp"
|
||||
log_warning " sudo ufw allow 443/tcp"
|
||||
echo ""
|
||||
log_success "Caddy will automatically obtain SSL certificates from Let's Encrypt!"
|
||||
else
|
||||
log_warning "NEXT STEPS:"
|
||||
log_warning " 1. Install Caddy: https://caddyserver.com/docs/install"
|
||||
log_warning " 2. Edit Caddyfile and replace 'your-domain.com' with your domain"
|
||||
log_warning " 3. Start Caddy: sudo caddy start"
|
||||
log_warning " (or for testing: caddy run)"
|
||||
log_warning " 4. Configure firewall to allow ports 80/443"
|
||||
fi
|
||||
echo ""
|
||||
log_info "Alternative web servers:"
|
||||
log_info " - See DEPLOYMENT.md for nginx/Apache configurations"
|
||||
echo ""
|
||||
|
||||
# Register cleanup on script exit
|
||||
|
|
|
|||
|
|
@ -8,6 +8,30 @@
|
|||
|
||||
set -e # Exit on error
|
||||
|
||||
# Parse command line arguments
|
||||
USE_SYSTEMD=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--systemd)
|
||||
USE_SYSTEMD=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --systemd Stop systemd service instead of PID-based process"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Color codes for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
|
|
@ -37,48 +61,87 @@ log_error() {
|
|||
# Stop Service
|
||||
################################################################################
|
||||
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
log_error "No PID file found at $PID_FILE"
|
||||
log_error "VidRip may not be running or was started manually"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$USE_SYSTEMD" = true ]; then
|
||||
# Systemd mode
|
||||
log_info "Stopping systemd service..."
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
# Check if running as root or with sudo
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
log_error "Systemd mode requires root privileges"
|
||||
log_error "Please run with: sudo $0 --systemd"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_error "Process $PID is not running"
|
||||
log_info "Removing stale PID file..."
|
||||
# Check if service exists
|
||||
if ! systemctl list-unit-files | grep -q "vidrip-backend.service"; then
|
||||
log_error "VidRip backend service is not installed"
|
||||
log_error "Service may have been started in nohup mode"
|
||||
log_info "Try running without --systemd flag"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop the service
|
||||
systemctl stop vidrip-backend
|
||||
|
||||
# Wait and verify
|
||||
sleep 1
|
||||
if systemctl is-active --quiet vidrip-backend; then
|
||||
log_error "Failed to stop service"
|
||||
log_error "Check status with: sudo systemctl status vidrip-backend"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "VidRip backend service stopped successfully"
|
||||
log_info "To disable auto-start on boot: sudo systemctl disable vidrip-backend"
|
||||
log_info "To view logs: sudo journalctl -u vidrip-backend"
|
||||
|
||||
else
|
||||
# Traditional PID-based stop
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
log_error "No PID file found at $PID_FILE"
|
||||
log_error "VidRip may not be running or was started with --systemd"
|
||||
log_info "Try running with --systemd flag if using systemd service"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
|
||||
if ! ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_error "Process $PID is not running"
|
||||
log_info "Removing stale PID file..."
|
||||
rm -f "$PID_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Stopping VidRip (PID: $PID)..."
|
||||
|
||||
# Try graceful shutdown first
|
||||
kill "$PID"
|
||||
|
||||
# Wait up to 10 seconds for graceful shutdown
|
||||
COUNTER=0
|
||||
while ps -p "$PID" > /dev/null 2>&1 && [ $COUNTER -lt 10 ]; do
|
||||
sleep 1
|
||||
COUNTER=$((COUNTER + 1))
|
||||
done
|
||||
|
||||
# Force kill if still running
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_info "Process did not stop gracefully, forcing shutdown..."
|
||||
kill -9 "$PID"
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Verify it's stopped
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_error "Failed to stop process $PID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove PID file
|
||||
rm -f "$PID_FILE"
|
||||
exit 1
|
||||
|
||||
log_success "VidRip stopped successfully"
|
||||
fi
|
||||
|
||||
log_info "Stopping VidRip (PID: $PID)..."
|
||||
|
||||
# Try graceful shutdown first
|
||||
kill "$PID"
|
||||
|
||||
# Wait up to 10 seconds for graceful shutdown
|
||||
COUNTER=0
|
||||
while ps -p "$PID" > /dev/null 2>&1 && [ $COUNTER -lt 10 ]; do
|
||||
sleep 1
|
||||
COUNTER=$((COUNTER + 1))
|
||||
done
|
||||
|
||||
# Force kill if still running
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_info "Process did not stop gracefully, forcing shutdown..."
|
||||
kill -9 "$PID"
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Verify it's stopped
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_error "Failed to stop process $PID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove PID file
|
||||
rm -f "$PID_FILE"
|
||||
|
||||
log_success "VidRip stopped successfully"
|
||||
exit 0
|
||||
|
|
|
|||
Loading…
Reference in New Issue