hannabaker commited on
Commit
e42a135
·
verified ·
1 Parent(s): 520d178

Create frontend/src/App.js

Browse files
Files changed (1) hide show
  1. frontend/src/App.js +482 -0
frontend/src/App.js ADDED
@@ -0,0 +1,482 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import {
3
+ Container,
4
+ Paper,
5
+ TextField,
6
+ Button,
7
+ Select,
8
+ MenuItem,
9
+ FormControl,
10
+ InputLabel,
11
+ Typography,
12
+ Box,
13
+ Grid,
14
+ Card,
15
+ CardContent,
16
+ LinearProgress,
17
+ Chip,
18
+ IconButton,
19
+ Alert,
20
+ Snackbar,
21
+ Dialog,
22
+ DialogTitle,
23
+ DialogContent,
24
+ DialogActions,
25
+ List,
26
+ ListItem,
27
+ ListItemText,
28
+ ListItemSecondaryAction,
29
+ Tabs,
30
+ Tab,
31
+ Switch,
32
+ FormControlLabel
33
+ } from '@mui/material';
34
+ import {
35
+ CloudUpload,
36
+ Cancel,
37
+ CheckCircle,
38
+ Error,
39
+ Refresh,
40
+ Settings,
41
+ Visibility,
42
+ VisibilityOff
43
+ } from '@mui/icons-material';
44
+ import axios from 'axios';
45
+ import io from 'socket.io-client';
46
+ import { v4 as uuidv4 } from 'uuid';
47
+
48
+ const API_URL = process.env.REACT_APP_API_URL || '';
49
+ const SOCKET_URL = process.env.REACT_APP_SOCKET_URL || '';
50
+
51
+ function App() {
52
+ const [vodUrl, setVodUrl] = useState('');
53
+ const [selectedProvider, setSelectedProvider] = useState('mega');
54
+ const [selectedFormat, setSelectedFormat] = useState('best');
55
+ const [providers, setProviders] = useState([]);
56
+ const [formats, setFormats] = useState([]);
57
+ const [credentials, setCredentials] = useState({
58
+ mega: { email: '', password: '' },
59
+ filen: { email: '', password: '' },
60
+ drime: { email: '', password: '' }
61
+ });
62
+ const [tasks, setTasks] = useState([]);
63
+ const [activeTab, setActiveTab] = useState(0);
64
+ const [showPassword, setShowPassword] = useState(false);
65
+ const [alert, setAlert] = useState({ open: false, message: '', severity: 'info' });
66
+ const [loading, setLoading] = useState(false);
67
+ const [autoScroll, setAutoScroll] = useState(true);
68
+
69
+ const socketRef = useRef(null);
70
+ const userIdRef = useRef(localStorage.getItem('userId') || uuidv4());
71
+
72
+ useEffect(() => {
73
+ // Save user ID
74
+ localStorage.setItem('userId', userIdRef.current);
75
+
76
+ // Load providers
77
+ loadProviders();
78
+
79
+ // Load tasks
80
+ loadTasks();
81
+
82
+ // Initialize socket connection
83
+ socketRef.current = io(SOCKET_URL);
84
+
85
+ socketRef.current.on('connect', () => {
86
+ console.log('Connected to server');
87
+ });
88
+
89
+ socketRef.current.on('task_update', (data) => {
90
+ updateTask(data.task_id, data);
91
+ });
92
+
93
+ return () => {
94
+ if (socketRef.current) {
95
+ socketRef.current.disconnect();
96
+ }
97
+ };
98
+ }, []);
99
+
100
+ const loadProviders = async () => {
101
+ try {
102
+ const response = await axios.get(`${API_URL}/api/providers`);
103
+ setProviders(response.data.providers);
104
+ } catch (error) {
105
+ showAlert('Failed to load providers', 'error');
106
+ }
107
+ };
108
+
109
+ const loadTasks = async () => {
110
+ try {
111
+ const response = await axios.get(`${API_URL}/api/tasks`, {
112
+ headers: { 'X-User-ID': userIdRef.current }
113
+ });
114
+ setTasks(response.data.tasks);
115
+
116
+ // Subscribe to active tasks
117
+ response.data.tasks.forEach(task => {
118
+ if (task.status === 'processing' || task.status === 'queued') {
119
+ socketRef.current.emit('subscribe_task', { task_id: task.id });
120
+ }
121
+ });
122
+ } catch (error) {
123
+ showAlert('Failed to load tasks', 'error');
124
+ }
125
+ };
126
+
127
+ const loadFormats = async () => {
128
+ if (!vodUrl) return;
129
+
130
+ setLoading(true);
131
+ try {
132
+ const response = await axios.get(`${API_URL}/api/formats/${encodeURIComponent(vodUrl)}`);
133
+ setFormats(response.data.formats);
134
+ showAlert(`Found ${response.data.formats.length} quality options`, 'success');
135
+ } catch (error) {
136
+ showAlert('Failed to load formats', 'error');
137
+ } finally {
138
+ setLoading(false);
139
+ }
140
+ };
141
+
142
+ const validateCredentials = async () => {
143
+ const creds = credentials[selectedProvider];
144
+ if (!creds.email || !creds.password) {
145
+ showAlert('Please enter credentials', 'warning');
146
+ return;
147
+ }
148
+
149
+ setLoading(true);
150
+ try {
151
+ const response = await axios.post(`${API_URL}/api/validate-credentials`, {
152
+ provider: selectedProvider,
153
+ credentials: creds
154
+ });
155
+
156
+ showAlert(response.data.message, response.data.valid ? 'success' : 'error');
157
+ } catch (error) {
158
+ showAlert('Validation failed', 'error');
159
+ } finally {
160
+ setLoading(false);
161
+ }
162
+ };
163
+
164
+ const startArchiving = async () => {
165
+ const creds = credentials[selectedProvider];
166
+ if (!vodUrl || !creds.email || !creds.password) {
167
+ showAlert('Please fill all required fields', 'warning');
168
+ return;
169
+ }
170
+
171
+ setLoading(true);
172
+ try {
173
+ const response = await axios.post(`${API_URL}/api/tasks`, {
174
+ vod_url: vodUrl,
175
+ provider: selectedProvider,
176
+ format_id: selectedFormat,
177
+ credentials: creds
178
+ }, {
179
+ headers: { 'X-User-ID': userIdRef.current }
180
+ });
181
+
182
+ const newTask = {
183
+ id: response.data.task_id,
184
+ vod_url: vodUrl,
185
+ provider: selectedProvider,
186
+ status: 'queued',
187
+ progress_data: { phase: 'queued', percent: 0 },
188
+ created_at: new Date().toISOString()
189
+ };
190
+
191
+ setTasks([newTask, ...tasks]);
192
+
193
+ // Subscribe to task updates
194
+ socketRef.current.emit('subscribe_task', { task_id: response.data.task_id });
195
+
196
+ showAlert('Task started successfully', 'success');
197
+
198
+ // Clear form
199
+ setVodUrl('');
200
+ setSelectedFormat('best');
201
+
202
+ } catch (error) {
203
+ showAlert('Failed to start task', 'error');
204
+ } finally {
205
+ setLoading(false);
206
+ }
207
+ };
208
+
209
+ const cancelTask = async (taskId) => {
210
+ try {
211
+ await axios.delete(`${API_URL}/api/tasks/${taskId}`);
212
+ updateTask(taskId, { status: 'cancelled' });
213
+ showAlert('Task cancelled', 'info');
214
+ } catch (error) {
215
+ showAlert('Failed to cancel task', 'error');
216
+ }
217
+ };
218
+
219
+ const updateTask = (taskId, updates) => {
220
+ setTasks(prevTasks =>
221
+ prevTasks.map(task =>
222
+ task.id === taskId ? { ...task, ...updates } : task
223
+ )
224
+ );
225
+ };
226
+
227
+ const showAlert = (message, severity = 'info') => {
228
+ setAlert({ open: true, message, severity });
229
+ };
230
+
231
+ const getStatusColor = (status) => {
232
+ switch (status) {
233
+ case 'completed': return 'success';
234
+ case 'failed': return 'error';
235
+ case 'processing': return 'primary';
236
+ case 'cancelled': return 'default';
237
+ default: return 'default';
238
+ }
239
+ };
240
+
241
+ const getStatusIcon = (status) => {
242
+ switch (status) {
243
+ case 'completed': return <CheckCircle />;
244
+ case 'failed': return <Error />;
245
+ case 'cancelled': return <Cancel />;
246
+ default: return null;
247
+ }
248
+ };
249
+
250
+ return (
251
+ <Container maxWidth="lg" sx={{ py: 4 }}>
252
+ <Paper elevation={3} sx={{ p: 4, mb: 4 }}>
253
+ <Typography variant="h4" component="h1" gutterBottom align="center">
254
+ VOD Archiver Pro
255
+ </Typography>
256
+ <Typography variant="subtitle1" align="center" color="text.secondary" gutterBottom>
257
+ Download and archive Twitch VODs to multiple cloud providers
258
+ </Typography>
259
+
260
+ <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
261
+ <Tabs value={activeTab} onChange={(e, v) => setActiveTab(v)}>
262
+ <Tab label="New Task" />
263
+ <Tab label="Active Tasks" />
264
+ <Tab label="History" />
265
+ </Tabs>
266
+ </Box>
267
+
268
+ {activeTab === 0 && (
269
+ <Box>
270
+ <Grid container spacing={3}>
271
+ <Grid item xs={12}>
272
+ <TextField
273
+ fullWidth
274
+ label="Twitch VOD URL"
275
+ value={vodUrl}
276
+ onChange={(e) => setVodUrl(e.target.value)}
277
+ placeholder="https://www.twitch.tv/videos/123456789"
278
+ InputProps={{
279
+ endAdornment: (
280
+ <Button onClick={loadFormats} disabled={!vodUrl || loading}>
281
+ Load Formats
282
+ </Button>
283
+ )
284
+ }}
285
+ />
286
+ </Grid>
287
+
288
+ <Grid item xs={12} md={6}>
289
+ <FormControl fullWidth>
290
+ <InputLabel>Provider</InputLabel>
291
+ <Select
292
+ value={selectedProvider}
293
+ onChange={(e) => setSelectedProvider(e.target.value)}
294
+ >
295
+ {providers.map(provider => (
296
+ <MenuItem key={provider.id} value={provider.id}>
297
+ {provider.name} - {provider.maxSize}
298
+ </MenuItem>
299
+ ))}
300
+ </Select>
301
+ </FormControl>
302
+ </Grid>
303
+
304
+ <Grid item xs={12} md={6}>
305
+ <FormControl fullWidth>
306
+ <InputLabel>Quality</InputLabel>
307
+ <Select
308
+ value={selectedFormat}
309
+ onChange={(e) => setSelectedFormat(e.target.value)}
310
+ >
311
+ {formats.length === 0 ? (
312
+ <MenuItem value="best">Best Quality (Source)</MenuItem>
313
+ ) : (
314
+ formats.map(format => (
315
+ <MenuItem key={format.format_id} value={format.format_id}>
316
+ {format.label}
317
+ </MenuItem>
318
+ ))
319
+ )}
320
+ </Select>
321
+ </FormControl>
322
+ </Grid>
323
+
324
+ <Grid item xs={12} md={6}>
325
+ <TextField
326
+ fullWidth
327
+ label="Email"
328
+ type="email"
329
+ value={credentials[selectedProvider].email}
330
+ onChange={(e) => setCredentials({
331
+ ...credentials,
332
+ [selectedProvider]: {
333
+ ...credentials[selectedProvider],
334
+ email: e.target.value
335
+ }
336
+ })}
337
+ />
338
+ </Grid>
339
+
340
+ <Grid item xs={12} md={6}>
341
+ <TextField
342
+ fullWidth
343
+ label="Password"
344
+ type={showPassword ? 'text' : 'password'}
345
+ value={credentials[selectedProvider].password}
346
+ onChange={(e) => setCredentials({
347
+ ...credentials,
348
+ [selectedProvider]: {
349
+ ...credentials[selectedProvider],
350
+ password: e.target.value
351
+ }
352
+ })}
353
+ InputProps={{
354
+ endAdornment: (
355
+ <IconButton onClick={() => setShowPassword(!showPassword)}>
356
+ {showPassword ? <VisibilityOff /> : <Visibility />}
357
+ </IconButton>
358
+ )
359
+ }}
360
+ />
361
+ </Grid>
362
+
363
+ <Grid item xs={12}>
364
+ <Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
365
+ <Button
366
+ variant="contained"
367
+ size="large"
368
+ onClick={startArchiving}
369
+ disabled={loading}
370
+ startIcon={<CloudUpload />}
371
+ >
372
+ Start Archiving
373
+ </Button>
374
+ <Button
375
+ variant="outlined"
376
+ size="large"
377
+ onClick={validateCredentials}
378
+ disabled={loading}
379
+ >
380
+ Validate Credentials
381
+ </Button>
382
+ </Box>
383
+ </Grid>
384
+ </Grid>
385
+ </Box>
386
+ )}
387
+
388
+ {(activeTab === 1 || activeTab === 2) && (
389
+ <Box>
390
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
391
+ <FormControlLabel
392
+ control={<Switch checked={autoScroll} onChange={(e) => setAutoScroll(e.target.checked)} />}
393
+ label="Auto-scroll logs"
394
+ />
395
+ <Button startIcon={<Refresh />} onClick={loadTasks}>
396
+ Refresh
397
+ </Button>
398
+ </Box>
399
+
400
+ <List>
401
+ {tasks
402
+ .filter(task => activeTab === 1 ?
403
+ ['queued', 'processing'].includes(task.status) :
404
+ ['completed', 'failed', 'cancelled'].includes(task.status)
405
+ )
406
+ .map(task => (
407
+ <Card key={task.id} sx={{ mb: 2 }}>
408
+ <CardContent>
409
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
410
+ <Typography variant="h6" noWrap sx={{ maxWidth: '60%' }}>
411
+ {task.vod_url}
412
+ </Typography>
413
+ <Chip
414
+ label={task.status}
415
+ color={getStatusColor(task.status)}
416
+ icon={getStatusIcon(task.status)}
417
+ />
418
+ </Box>
419
+
420
+ <Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
421
+ <Chip label={task.provider} size="small" />
422
+ <Typography variant="caption" color="text.secondary">
423
+ {new Date(task.created_at).toLocaleString()}
424
+ </Typography>
425
+ </Box>
426
+
427
+ {task.status === 'processing' && task.progress_data && (
428
+ <Box sx={{ mb: 2 }}>
429
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
430
+ <Typography variant="body2">
431
+ {task.progress_data.phase === 'downloading' ? 'Downloading' : 'Uploading'}
432
+ </Typography>
433
+ <Typography variant="body2">
434
+ {Math.round(task.progress_data.percent)}%
435
+ </Typography>
436
+ </Box>
437
+ <LinearProgress
438
+ variant="determinate"
439
+ value={task.progress_data.percent}
440
+ />
441
+ </Box>
442
+ )}
443
+
444
+ {task.error && (
445
+ <Alert severity="error" sx={{ mt: 2 }}>
446
+ {task.error}
447
+ </Alert>
448
+ )}
449
+
450
+ {task.status === 'processing' && (
451
+ <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
452
+ <Button
453
+ color="error"
454
+ onClick={() => cancelTask(task.id)}
455
+ startIcon={<Cancel />}
456
+ >
457
+ Cancel
458
+ </Button>
459
+ </Box>
460
+ )}
461
+ </CardContent>
462
+ </Card>
463
+ ))}
464
+ </List>
465
+ </Box>
466
+ )}
467
+ </Paper>
468
+
469
+ <Snackbar
470
+ open={alert.open}
471
+ autoHideDuration={6000}
472
+ onClose={() => setAlert({ ...alert, open: false })}
473
+ >
474
+ <Alert severity={alert.severity} onClose={() => setAlert({ ...alert, open: false })}>
475
+ {alert.message}
476
+ </Alert>
477
+ </Snackbar>
478
+ </Container>
479
+ );
480
+ }
481
+
482
+ export default App;