Claude's first attempt on webdav
This commit is contained in:
parent
8bb3d1f9a7
commit
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
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
// 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);
|
||||
|
||||
// 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() {
|
|||
},
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue