Compare commits

...

9 Commits
main ... webdav

17 changed files with 1360 additions and 108 deletions

View File

@ -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

113
Caddyfile Normal file
View File

@ -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
# }
# }

View File

@ -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

View File

@ -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",

View File

@ -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(

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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;
}

View File

@ -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'
];

View File

@ -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">

View File

@ -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>

View File

@ -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),
}),
};

View File

@ -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 {

View File

@ -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

View File

@ -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