Claude's first attempt on webdav

This commit is contained in:
Ryan Whytsell 2025-10-21 13:42:04 -04:00
parent 8bb3d1f9a7
commit 8a790043f8
Signed by: Epithium
GPG Key ID: 940AC18C08E925EA
11 changed files with 713 additions and 13 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: Routes are split by domain:
- `backend/src/routes/channels.ts`: Channel CRUD + refresh - `backend/src/routes/channels.ts`: Channel CRUD + refresh
- `backend/src/routes/videos.ts`: Video CRUD + retry - `backend/src/routes/videos.ts`: Video CRUD + retry + streaming
- `backend/src/routes/config.ts`: Settings + scheduler control - `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 ## Key Constraints
@ -103,7 +103,8 @@ Adding a channel fetches ALL videos from that channel using yt-dlp's `--flat-pla
## File Locations ## File Locations
- Database: `backend/data.db` - 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) - Environment: Optional `backend/.env` (PORT, NODE_ENV)
## System Requirements ## System Requirements
@ -153,3 +154,90 @@ For persistent 403 errors, export cookies from your browser:
- Increase download interval to avoid rate limiting - Increase download interval to avoid rate limiting
- Use variance to randomize download times - Use variance to randomize download times
- Avoid downloading too many videos from the same channel quickly - 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

View File

@ -13,7 +13,8 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"better-sqlite3": "^9.2.2", "better-sqlite3": "^9.2.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"dotenv": "^16.3.1" "dotenv": "^16.3.1",
"webdav": "^5.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21", "@types/express": "^4.17.21",

View File

@ -89,7 +89,12 @@ export function initDatabase() {
intervalMinutes: '180', // 180 minutes = 3 hours intervalMinutes: '180', // 180 minutes = 3 hours
varianceMinutes: '30', varianceMinutes: '30',
maxConcurrentDownloads: '1', maxConcurrentDownloads: '1',
enabled: 'true' enabled: 'true',
webdavEnabled: 'false',
webdavUrl: '',
webdavUsername: '',
webdavPassword: '',
webdavPath: '/vidrip/'
}; };
const insertConfig = db.prepare( const insertConfig = db.prepare(

View File

@ -1,6 +1,7 @@
import { Router } from 'express'; import { Router } from 'express';
import { configOperations } from '../db/database'; import { configOperations } from '../db/database';
import { restartScheduler, getSchedulerStatus } from '../services/scheduler'; import { restartScheduler, getSchedulerStatus } from '../services/scheduler';
import { testConnection } from '../services/webdav';
const router = Router(); 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; export default router;

View File

@ -1,6 +1,7 @@
import { Router } from 'express'; import { Router } from 'express';
import { videoOperations } from '../db/database'; import { videoOperations } from '../db/database';
import { getVideoInfo, downloadVideo } from '../services/ytdlp'; 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 fs from 'fs';
import path from 'path'; import path from 'path';
@ -90,7 +91,7 @@ router.post('/', async (req, res) => {
}); });
// Delete video // Delete video
router.delete('/:id', (req, res) => { router.delete('/:id', async (req, res) => {
try { try {
const id = parseInt(req.params.id); const id = parseInt(req.params.id);
@ -99,9 +100,22 @@ router.delete('/:id', (req, res) => {
return res.status(404).json({ error: 'Video not found' }); return res.status(404).json({ error: 'Video not found' });
} }
// Delete file if it exists // Delete file based on storage type
if (video.filePath && fs.existsSync(video.filePath)) { 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); fs.unlinkSync(video.filePath);
console.log(`Deleted local file: ${video.filePath}`);
}
} }
videoOperations.delete(id); videoOperations.delete(id);
@ -164,7 +178,37 @@ router.post('/:id/download', async (req, res) => {
videoOperations.updateStatus(id, 'downloading'); videoOperations.updateStatus(id, 'downloading');
const result = await downloadVideo(video.videoId, video.url); 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); videoOperations.markCompleted(id, result.filePath, result.fileSize);
}
console.log(`Immediate download completed: ${video.title}`); console.log(`Immediate download completed: ${video.title}`);
} catch (error) { } catch (error) {
console.error('Immediate download error:', 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; export default router;

View File

@ -6,6 +6,8 @@ import {
} from "../db/database"; } from "../db/database";
import { downloadVideo, getChannelVideos } from "./ytdlp"; import { downloadVideo, getChannelVideos } from "./ytdlp";
import { DownloadProgress } from "../types"; import { DownloadProgress } from "../types";
import { isWebDAVEnabled, uploadVideo as uploadToWebDAV } from "./webdav";
import fs from "fs";
let schedulerTask: cron.ScheduledTask | null = null; let schedulerTask: cron.ScheduledTask | null = null;
let isDownloading = false; 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); videoOperations.markCompleted(video.id, result.filePath, result.fileSize);
}
console.log(`Download completed: ${video.title}`); console.log(`Download completed: ${video.title}`);
currentProgress = null; currentProgress = null;
} catch (error) { } 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

@ -9,6 +9,8 @@ function SettingsPage() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [success, setSuccess] = useState(''); const [success, setSuccess] = useState('');
const [testingWebDAV, setTestingWebDAV] = useState(false);
const [webdavTestResult, setWebdavTestResult] = useState<{ success: boolean; message: string } | null>(null);
useEffect(() => { useEffect(() => {
loadConfig(); loadConfig();
@ -61,6 +63,31 @@ function SettingsPage() {
const handleChange = (key: keyof Config, value: string) => { const handleChange = (key: keyof Config, value: string) => {
if (!config) return; if (!config) return;
setConfig({ ...config, [key]: value }); 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) { if (loading) {
@ -224,6 +251,117 @@ function SettingsPage() {
Number of videos to download simultaneously (recommended: 1) Number of videos to download simultaneously (recommended: 1)
</p> </p>
</div> </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>
<div className="mt-6 flex justify-end"> <div className="mt-6 flex justify-end">

View File

@ -98,7 +98,7 @@ function VideoPlayerPage() {
<video <video
controls controls
className="w-full h-full" className="w-full h-full"
src={`/downloads/${video.videoId}.mp4`} src={`/api/videos/${video.id}/stream`}
> >
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>

View File

@ -135,4 +135,10 @@ export const configAPI = {
getSchedulerStatus: () => getSchedulerStatus: () =>
fetchJSON<SchedulerStatus>(`${API_BASE}/config/scheduler/status`), 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; varianceMinutes: string;
maxConcurrentDownloads: string; maxConcurrentDownloads: string;
enabled: string; enabled: string;
webdavEnabled: string;
webdavUrl: string;
webdavUsername: string;
webdavPassword: string;
webdavPath: string;
} }
export interface SchedulerStatus { export interface SchedulerStatus {