add react web project

This commit is contained in:
YouROK
2020-12-24 12:59:16 +03:00
parent 4e816f2cae
commit a404545133
26 changed files with 20515 additions and 2 deletions

93
web/src/components/Add.js Normal file
View File

@@ -0,0 +1,93 @@
import React from 'react'
import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import LibraryAddIcon from '@material-ui/icons/LibraryAdd'
import ListItemText from '@material-ui/core/ListItemText'
import ListItem from '@material-ui/core/ListItem'
import { torrentsHost } from '../utils/Hosts'
export default function AddDialog() {
const [open, setOpen] = React.useState(false)
const [magnet, setMagnet] = React.useState('')
const [title, setTitle] = React.useState('')
const [poster, setPoster] = React.useState('')
const handleClickOpen = () => {
setOpen(true)
}
const inputMagnet = (event) => {
setMagnet(event.target.value)
}
const inputTitle = (event) => {
setTitle(event.target.value)
}
const inputPoster = (event) => {
setPoster(event.target.value)
}
const handleCloseSave = () => {
try {
if (!magnet) return
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({
action: 'add',
link: magnet,
title: title,
poster: poster,
save_to_db: true,
}),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
setOpen(false)
} catch (e) {
console.log(e)
}
}
const handleClose = () => {
setOpen(false)
}
return (
<div>
<ListItem button key="Add" onClick={handleClickOpen}>
<ListItemIcon>
<LibraryAddIcon />
</ListItemIcon>
<ListItemText primary="Add" />
</ListItem>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth={true}>
<DialogTitle id="form-dialog-title">Add Magnet</DialogTitle>
<DialogContent>
<DialogContentText>Add magnet or link to torrent file:</DialogContentText>
<TextField onChange={inputTitle} margin="dense" id="title" label="Title" type="text" fullWidth />
<TextField onChange={inputPoster} margin="dense" id="poster" label="Poster" type="url" fullWidth />
<TextField onChange={inputMagnet} autoFocus margin="dense" id="magnet" label="Magnet" type="text" fullWidth />
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary" variant="outlined">
Cancel
</Button>
<Button onClick={handleCloseSave} color="primary" variant="outlined">
Add
</Button>
</DialogActions>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,191 @@
import React, { useEffect } from 'react'
import clsx from 'clsx'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import Drawer from '@material-ui/core/Drawer'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import List from '@material-ui/core/List'
import CssBaseline from '@material-ui/core/CssBaseline'
import Typography from '@material-ui/core/Typography'
import Divider from '@material-ui/core/Divider'
import IconButton from '@material-ui/core/IconButton'
import MenuIcon from '@material-ui/icons/Menu'
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
import ChevronRightIcon from '@material-ui/icons/ChevronRight'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import ListIcon from '@material-ui/icons/List'
import PowerSettingsNewIcon from '@material-ui/icons/PowerSettingsNew'
import TorrentList from './TorrentList'
import { Box } from '@material-ui/core'
import AddDialog from './Add'
import RemoveAll from './RemoveAll'
import SettingsDialog from './Settings'
import { playlistAllHost, shutdownHost, torrserverHost } from '../utils/Hosts'
import DonateDialog from './Donate'
import UploadDialog from './Upload'
const drawerWidth = 240
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
hide: {
display: 'none',
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
},
drawerOpen: {
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: theme.spacing(7) + 1,
[theme.breakpoints.up('sm')]: {
width: theme.spacing(9) + 1,
},
},
toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
}))
export default function MiniDrawer() {
const classes = useStyles()
const theme = useTheme()
const [open, setOpen] = React.useState(false)
const [tsVersion, setTSVersion] = React.useState('')
const handleDrawerOpen = () => {
setOpen(true)
}
const handleDrawerClose = () => {
setOpen(false)
}
useEffect(() => {
fetch(torrserverHost + '/echo')
.then((resp) => resp.text())
.then((txt) => {
if (!txt.startsWith('<!DOCTYPE html>')) setTSVersion(txt)
})
}, [open])
return (
<div className={classes.root}>
<CssBaseline />
<AppBar
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton, {
[classes.hide]: open,
})}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap>
TorrServer {tsVersion}
</Typography>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}</IconButton>
</div>
<Divider />
<List>
<AddDialog />
<UploadDialog />
<RemoveAll />
<DonateDialog />
</List>
<Divider />
<List>
<ListItem button key="Playlist all torrents" onClick={() => window.open(playlistAllHost(), '_blank')}>
<ListItemIcon>
<ListIcon />
</ListItemIcon>
<ListItemText primary="Playlist all torrents" />
</ListItem>
<SettingsDialog />
<ListItem button key="Close server" onClick={() => fetch(shutdownHost())}>
<ListItemIcon>
<PowerSettingsNewIcon />
</ListItemIcon>
<ListItemText primary="Close server" />
</ListItem>
</List>
<Divider />
</Drawer>
<main className={classes.content}>
<Box m="5em" />
<TorrentList />
</main>
</div>
)
}

View File

@@ -0,0 +1,130 @@
import React, { useEffect, useRef } from 'react'
import Typography from '@material-ui/core/Typography'
import { getPeerString, humanizeSize } from '../utils/Utils'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import { cacheHost } from '../utils/Hosts'
export default function DialogCacheInfo(props) {
const [hash] = React.useState(props.hash)
const [cache, setCache] = React.useState({})
const timerID = useRef(-1)
useEffect(() => {
if (hash)
timerID.current = setInterval(() => {
getCache(hash, (cache) => {
setCache(cache)
})
}, 1000)
else clearInterval(timerID.current)
return () => {
clearInterval(timerID.current)
}
}, [hash, props.open])
return (
<div>
<DialogTitle id="form-dialog-title">
<Typography fullWidth>
<b>Hash </b> {cache.Hash}
<br />
<b>Capacity </b> {humanizeSize(cache.Capacity)}
<br />
<b>Filled </b> {humanizeSize(cache.Filled)}
<br />
<b>Torrent size </b> {cache.Torrent && cache.Torrent.torrent_size && humanizeSize(cache.Torrent.torrent_size)}
<br />
<b>Pieces length </b> {humanizeSize(cache.PiecesLength)}
<br />
<b>Pieces count </b> {cache.PiecesCount}
<br />
<b>Peers: </b> {getPeerString(cache.Torrent)}
<br />
<b>Download speed </b> {cache.Torrent && cache.Torrent.download_speed ? humanizeSize(cache.Torrent.download_speed) + '/sec' : ''}
<br />
<b>Status </b> {cache.Torrent && cache.Torrent.stat_string && cache.Torrent.stat_string}
</Typography>
</DialogTitle>
<DialogContent>
<div className="cache" dangerouslySetInnerHTML={{ __html: getCacheMap(cache) }} />
</DialogContent>
</div>
)
}
function getCacheMap(cache) {
if (!cache || !cache.PiecesCount) return ''
var html = ''
for (let i = 0; i < cache.PiecesCount; i++) {
html += "<span class='piece"
let info = i
if (cache.Pieces && cache.Pieces[i]) {
let piece = cache.Pieces[i]
if (piece.Completed && piece.Size >= piece.Length) {
html += ' piece-complete'
info += ' 100%'
}else {
html += ' piece-loading'
info += ' ' + (cache.Pieces[i].Size/cache.Pieces[i].Length*100).toFixed(2) + '%'
}
cache.Readers.forEach((r,k)=> {
if (i >= r.Start && i <= r.End && i != r.Reader)
html += ' reader-range'
if (i == r.Reader) {
html += ' piece-reader'
info += ' reader'
}
})
}
html += "' title='" + info + "'></span>"
}
return html
}
function getCache(hash, callback) {
try {
fetch(cacheHost(), {
method: 'post',
body: JSON.stringify({ action: 'get', hash: hash }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then(
(json) => {
callback(json)
},
(error) => {
callback({})
console.error(error)
}
)
} catch (e) {
console.error(e)
callback({})
}
}
/*
{
"Hash": "41e36c8de915d80db83fc134bee4e7e2d292657e",
"Capacity": 209715200,
"Filled": 2914808,
"PiecesLength": 4194304,
"PiecesCount": 2065,
"DownloadSpeed": 32770.860273455524,
"Pieces": {
"2064": {
"Id": 2064,
"Length": 2914808,
"Size": 162296,
"Completed": false
}
}
}
*/

View File

@@ -0,0 +1,95 @@
import React, { useEffect } from 'react'
import Typography from '@material-ui/core/Typography'
import { Button, ButtonGroup, Grid, List, ListItem } from '@material-ui/core'
import CachedIcon from '@material-ui/icons/Cached'
import { getPeerString, humanizeSize } from '../utils/Utils'
import { playlistTorrHost, streamHost } from '../utils/Hosts'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
const style = {
width100: {
width: '100%',
},
width80: {
width: '80%',
},
poster: {
display: 'flex',
flexDirection: 'row',
},
}
export default function DialogTorrentInfo(props) {
const [torrent, setTorrent] = React.useState(props.torrent)
useEffect(() => {
setTorrent(props.torrent)
}, [props.torrent, props.open])
return (
<div>
<DialogTitle id="form-dialog-title">
<Grid container spacing={1}>
<Grid item>{torrent.poster && <img alt="" height="200" align="left" style={style.poster} src={torrent.poster} />}</Grid>
<Grid style={style.width80} item>
{torrent.title} {torrent.name && torrent.name !== torrent.title && ' | ' + torrent.name}
<Typography>
<b>Peers: </b> {getPeerString(torrent)}
<br />
<b>Loaded: </b> {getPreload(torrent)}
<br />
<b>Speed: </b> {humanizeSize(torrent.download_speed)}
<br />
<b>Status: </b> {torrent.stat_string}
<br />
</Typography>
</Grid>
</Grid>
</DialogTitle>
<DialogContent>
<List>
<ListItem>
<ButtonGroup style={style.width100} variant="contained" color="primary" aria-label="contained primary button group">
<Button style={style.width100} href={playlistTorrHost() + '/' + encodeURI(torrent.name || torrent.title || 'file') + '.m3u?link=' + torrent.hash + '&m3u'}>
Playlist
</Button>
<Button style={style.width100} href={playlistTorrHost() + '/' + encodeURI(torrent.name || torrent.title || 'file') + '.m3u?link=' + torrent.hash + '&m3u&fromlast'}>
Playlist after last view
</Button>
</ButtonGroup>
</ListItem>
{torrent.file_stats &&
torrent.file_stats.map((file) => (
<ButtonGroup style={style.width100} disableElevation variant="contained" color="primary">
<Button
style={style.width100}
href={streamHost() + '/' + encodeURI(file.path.split('\\').pop().split('/').pop()) + '?link=' + torrent.hash + '&index=' + file.id + '&play'}
>
<Typography>
{file.path.split('\\').pop().split('/').pop()} | {humanizeSize(file.length)}
</Typography>
</Button>
<Button onClick={() => fetch(streamHost() + '?link=' + torrent.hash + '&index=' + file.id + '&preload')}>
<CachedIcon />
<Typography>Preload</Typography>
</Button>
</ButtonGroup>
))}
</List>
</DialogContent>
</div>
)
}
function getPreload(torrent) {
if (torrent.preloaded_bytes > 0 && torrent.preload_size > 0 && torrent.preloaded_bytes < torrent.preload_size) {
let progress = ((torrent.preloaded_bytes * 100) / torrent.preload_size).toFixed(2)
return humanizeSize(torrent.preloaded_bytes) + ' / ' + humanizeSize(torrent.preload_size) + ' ' + progress + '%'
}
if (!torrent.preloaded_bytes) return humanizeSize(0)
return humanizeSize(torrent.preloaded_bytes)
}

View File

@@ -0,0 +1,59 @@
import React from 'react'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import Dialog from '@material-ui/core/Dialog'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import DialogActions from '@material-ui/core/DialogActions'
import Button from '@material-ui/core/Button'
import CreditCardIcon from '@material-ui/icons/CreditCard'
import List from '@material-ui/core/List'
import ButtonGroup from '@material-ui/core/ButtonGroup'
const donateFrame =
'<iframe src="https://yoomoney.ru/quickpay/shop-widget?writer=seller&targets=%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%BA%D0%B0%20%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%BE%D0%B2&targets-hint=&default-sum=200&button-text=14&payment-type-choice=on&mobile-payment-type-choice=on&comment=on&hint=&successURL=&quickpay=shop&account=410013733697114" width="100%" height="302" frameborder="0" allowtransparency="true" scrolling="no"></iframe>'
export default function DonateDialog() {
const [open, setOpen] = React.useState(false)
const handleClickOpen = () => {
setOpen(true)
}
const handleClose = () => {
setOpen(false)
}
return (
<div>
<ListItem button key="Donate" onClick={handleClickOpen}>
<ListItemIcon>
<CreditCardIcon />
</ListItemIcon>
<ListItemText primary="Donate" />
</ListItem>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth>
<DialogTitle id="form-dialog-title">Donate</DialogTitle>
<DialogContent>
<List>
<ListItem>
<ButtonGroup variant="outlined" color="primary" aria-label="contained primary button group">
<Button onClick={() => window.open('https://www.paypal.com/paypalme/yourok', '_blank')}>PayPal</Button>
<Button onClick={() => window.open('https://yoomoney.ru/to/410013733697114', '_blank')}>Yandex.Money</Button>
</ButtonGroup>
</ListItem>
<ListItem>
<div dangerouslySetInnerHTML={{ __html: donateFrame }} />
</ListItem>
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary" variant="outlined">
Ok
</Button>
</DialogActions>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,40 @@
import React from 'react'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import DeleteIcon from '@material-ui/icons/Delete'
import { torrentsHost } from '../utils/Hosts'
export default function RemoveAll() {
const fnRemoveAll = () => {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'list' }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then((json) => {
json.forEach((torr) => {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'rem', hash: torr.hash }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
})
})
}
return (
<ListItem button key="Remove all" onClick={fnRemoveAll}>
<ListItemIcon>
<DeleteIcon />
</ListItemIcon>
<ListItemText primary="Remove all" />
</ListItem>
)
}

View File

@@ -0,0 +1,180 @@
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import React, { useEffect } from 'react'
import SettingsIcon from '@material-ui/icons/Settings'
import Dialog from '@material-ui/core/Dialog'
import DialogTitle from '@material-ui/core/DialogTitle'
import DialogContent from '@material-ui/core/DialogContent'
import TextField from '@material-ui/core/TextField'
import DialogActions from '@material-ui/core/DialogActions'
import Button from '@material-ui/core/Button'
import { FormControlLabel, InputLabel, Select, Switch } from '@material-ui/core'
import { settingsHost, setTorrServerHost, torrserverHost } from '../utils/Hosts'
export default function SettingsDialog() {
const [open, setOpen] = React.useState(false)
const [settings, setSets] = React.useState({})
const [show, setShow] = React.useState(false)
const [tsHost, setTSHost] = React.useState(torrserverHost ? torrserverHost : window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : ''))
const handleClickOpen = () => {
setOpen(true)
}
const handleClose = () => {
setOpen(false)
}
const handleCloseSave = () => {
setOpen(false)
let sets = JSON.parse(JSON.stringify(settings))
sets.CacheSize *= 1024 * 1024
sets.PreloadBufferSize *= 1024 * 1024
fetch(settingsHost(), {
method: 'post',
body: JSON.stringify({ action: 'set', sets: sets }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
}
useEffect(() => {
fetch(settingsHost(), {
method: 'post',
body: JSON.stringify({ action: 'get' }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then(
(json) => {
json.CacheSize /= 1024 * 1024
json.PreloadBufferSize /= 1024 * 1024
setSets(json)
setShow(true)
},
(error) => {
setShow(false)
console.log(error)
}
)
.catch((e) => {
setShow(false)
console.log(e)
})
}, [tsHost])
const onInputHost = (event) => {
let host = event.target.value
setTorrServerHost(host)
setTSHost(host)
}
const inputForm = (event) => {
let sets = JSON.parse(JSON.stringify(settings))
if (event.target.type === 'number' || event.target.type === 'select-one') {
sets[event.target.id] = Number(event.target.value)
} else if (event.target.type === 'checkbox') {
sets[event.target.id] = Boolean(event.target.checked)
}
setSets(sets)
}
return (
<div>
<ListItem button key="Settings" onClick={handleClickOpen}>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItem>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title" fullWidth={true}>
<DialogTitle id="form-dialog-title">Settings</DialogTitle>
<DialogContent>
<TextField onChange={onInputHost} margin="dense" id="TorrServerHost" label="Host" value={tsHost} type="url" fullWidth />
{show && (
<>
<TextField onChange={inputForm} margin="dense" id="CacheSize" label="Cache size" value={settings.CacheSize} type="number" fullWidth />
<FormControlLabel control={<Switch checked={settings.PreloadBuffer} onChange={inputForm} id="PreloadBuffer" color="primary" />} label="Preload buffer" />
<TextField onChange={inputForm} margin="dense" id="ReaderReadAHead" label="Reader readahead" value={settings.ReaderReadAHead} type="number" fullWidth />
<h1 />
<InputLabel htmlFor="RetrackersMode">Retracker mode</InputLabel>
<Select onChange={inputForm} type="number" native="true" id="RetrackersMode" value={settings.RetrackersMode}>
<option value={0}>Don't add retrackers</option>
<option value={1}>Add retrackers</option>
<option value={2}>Remove retrackers</option>
<option value={3}>Replace retrackers</option>
</Select>
<TextField
onChange={inputForm}
margin="dense"
id="TorrentDisconnectTimeout"
label="Torrent disconnect timeout"
value={settings.TorrentDisconnectTimeout}
type="number"
fullWidth
/>
<FormControlLabel control={<Switch checked={settings.EnableIPv6} onChange={inputForm} id="EnableIPv6" color="primary" />} label="Enable IPv6" />
<br />
<FormControlLabel control={<Switch checked={settings.ForceEncrypt} onChange={inputForm} id="ForceEncrypt" color="primary" />} label="Force encrypt" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableTCP} onChange={inputForm} id="DisableTCP" color="primary" />} label="Disable TCP" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableUTP} onChange={inputForm} id="DisableUTP" color="primary" />} label="Disable UTP" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableUPNP} onChange={inputForm} id="DisableUPNP" color="primary" />} label="Disable UPNP" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableDHT} onChange={inputForm} id="DisableDHT" color="primary" />} label="Disable DHT" />
<br />
<FormControlLabel control={<Switch checked={settings.DisableUpload} onChange={inputForm} id="DisableUpload" color="primary" />} label="Disable upload" />
<br />
<TextField onChange={inputForm} margin="dense" id="DownloadRateLimit" label="Download rate limit" value={settings.DownloadRateLimit} type="number" fullWidth />
<TextField onChange={inputForm} margin="dense" id="UploadRateLimit" label="Upload rate limit" value={settings.UploadRateLimit} type="number" fullWidth />
<TextField onChange={inputForm} margin="dense" id="ConnectionsLimit" label="Connections limit" value={settings.ConnectionsLimit} type="number" fullWidth />
<TextField onChange={inputForm} margin="dense" id="DhtConnectionLimit" label="Dht connection limit" value={settings.DhtConnectionLimit} type="number" fullWidth />
<TextField onChange={inputForm} margin="dense" id="PeersListenPort" label="Peers listen port" value={settings.PeersListenPort} type="number" fullWidth />
<h1 />
<InputLabel id="Strategy">Strategy</InputLabel>
<Select onChange={inputForm} type="number" native="true" id="Strategy" value={settings.Strategy}>
<option value={0}>DuplicateRequestTimeout</option>
<option value={1}>Fuzzing</option>
<option value={2}>Fastest</option>
</Select>
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary" variant="outlined">
Cancel
</Button>
<Button onClick={handleCloseSave} color="primary" variant="outlined">
Save
</Button>
</DialogActions>
</Dialog>
</div>
)
}
/*
{
"CacheSize": 209715200,
"PreloadBufferSize": 20971520,
"RetrackersMode": 1,
"TorrentDisconnectTimeout": 30,
"EnableIPv6": false,
"DisableTCP": false,
"DisableUTP": true,
"DisableUPNP": false,
"DisableDHT": false,
"DisableUpload": false,
"DownloadRateLimit": 0,
"UploadRateLimit": 0,
"ConnectionsLimit": 20,
"DhtConnectionLimit": 500,
"PeersListenPort": 0
}
*/

View File

@@ -0,0 +1,224 @@
import React, { useEffect, useRef } from 'react'
import ButtonGroup from '@material-ui/core/ButtonGroup'
import Button from '@material-ui/core/Button'
import 'fontsource-roboto'
import DeleteIcon from '@material-ui/icons/Delete'
import Typography from '@material-ui/core/Typography'
import ListItem from '@material-ui/core/ListItem'
import DialogActions from '@material-ui/core/DialogActions'
import Dialog from '@material-ui/core/Dialog'
import { humanizeSize } from '../utils/Utils'
import DialogTorrentInfo from './DialogTorrentInfo'
import { torrentsHost } from '../utils/Hosts'
import DialogCacheInfo from './DialogCacheInfo'
import DataUsageIcon from '@material-ui/icons/DataUsage'
const style = {
width100: {
width: '100%',
},
}
export default function Torrent(props) {
const [open, setOpen] = React.useState(false)
const [showCache, setShowCache] = React.useState(false)
const [torrent, setTorrent] = React.useState(props.torrent)
const timerID = useRef(-1)
useEffect(() => {
setTorrent(props.torrent)
}, [props.torrent])
useEffect(() => {
if (open)
timerID.current = setInterval(() => {
getTorrent(torrent.hash, (torr, error) => {
if (error) console.error(error)
else if (torr) setTorrent(torr)
})
}, 1000)
else clearInterval(timerID.current)
return () => {
clearInterval(timerID.current)
}
}, [torrent.hash, open])
return (
<div>
<ListItem>
<ButtonGroup style={style.width100} disableElevation variant="contained" color="primary">
<Button
style={style.width100}
onClick={() => {
setShowCache(false)
setOpen(true)
}}
>
<Typography>
{torrent.name ? torrent.name : torrent.title}
{torrent.torrent_size > 0 ? ' | ' + humanizeSize(torrent.torrent_size) : ''}
{torrent.download_speed > 0 ? ' | ' + humanizeSize(torrent.download_speed) + '/sec' : ''}
</Typography>
</Button>
<Button
onClick={() => {
setShowCache(true)
setOpen(true)
}}
>
<DataUsageIcon />
<Typography>Cache</Typography>
</Button>
<Button
onClick={() => {
deleteTorrent(torrent)
}}
>
<DeleteIcon />
<Typography>Delete</Typography>
</Button>
</ButtonGroup>
</ListItem>
<Dialog
open={open}
onClose={() => {
setOpen(false)
}}
aria-labelledby="form-dialog-title"
fullWidth={true}
maxWidth={'lg'}
>
{!showCache ? <DialogTorrentInfo torrent={(open, torrent)} /> : <DialogCacheInfo hash={(open, torrent.hash)} />}
<DialogActions>
<Button
variant="outlined"
color="primary"
onClick={() => {
setOpen(false)
}}
>
OK
</Button>
<Button
variant="outlined"
color="primary"
onClick={() => {
setOpen(false)
dropTorrent(torrent)
}}
>
Drop
</Button>
</DialogActions>
</Dialog>
</div>
)
}
function getTorrent(hash, callback) {
try {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'get', hash: hash }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then(
(json) => {
callback(json, null)
},
(error) => {
callback(null, error)
}
)
} catch (e) {
console.error(e)
}
}
function deleteTorrent(torrent) {
try {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({
action: 'rem',
hash: torrent.hash,
}),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
} catch (e) {
console.error(e)
}
}
function dropTorrent(torrent) {
try {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({
action: 'drop',
hash: torrent.hash,
}),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
} catch (e) {
console.error(e)
}
}
/*
{
"title": "Mulan 2020",
"poster": "https://kinohod.ru/o/88/d3/88d3054f-8fd3-4daf-8977-bb4bc8b95206.jpg",
"timestamp": 1606897747,
"name": "Mulan.2020.MVO.BDRip.1.46Gb",
"hash": "f6c992b437c04d0f5a44b42852bb61de7ce90f9a",
"stat": 2,
"stat_string": "Torrent preload",
"loaded_size": 6160384,
"torrent_size": 1569489783,
"preloaded_bytes": 5046272,
"preload_size": 20971520,
"download_speed": 737156.3390754947,
"total_peers": 149,
"pending_peers": 136,
"active_peers": 10,
"connected_seeders": 9,
"half_open_peers": 15,
"bytes_written": 100327,
"bytes_read": 8077590,
"bytes_read_data": 7831552,
"bytes_read_useful_data": 6160384,
"chunks_read": 478,
"chunks_read_useful": 376,
"chunks_read_wasted": 102,
"pieces_dirtied_good": 2,
"file_stats": [{
"id": 1,
"path": "Mulan.2020.MVO.BDRip.1.46Gb/Mulan.2020.MVO.BDRip.1.46Gb.avi",
"length": 1569415168
}, {
"id": 2,
"path": "Mulan.2020.MVO.BDRip.1.46Gb/Mulan.2020.MVO.BDRip.1.46Gb_forced.rus.srt",
"length": 765
}, {
"id": 3,
"path": "Mulan.2020.MVO.BDRip.1.46Gb/Mulan.2020.MVO.BDRip.1.46Gb_full.rus.srt",
"length": 73850
}]
}
*/

View File

@@ -0,0 +1,52 @@
import React, { useEffect, useRef } from 'react'
import Container from '@material-ui/core/Container'
import Torrent from './Torrent'
import List from '@material-ui/core/List'
import { Typography } from '@material-ui/core'
import { torrentsHost } from '../utils/Hosts'
export default function TorrentList(props, onChange) {
const [torrents, setTorrents] = React.useState([])
const [offline, setOffline] = React.useState(true)
const timerID = useRef(-1)
useEffect(() => {
timerID.current = setInterval(() => {
getTorrentList((torrs) => {
if (torrs) setOffline(false)
else setOffline(true)
setTorrents(torrs)
})
}, 1000)
return () => {
clearInterval(timerID.current)
}
}, [])
return (
<React.Fragment>
<Container maxWidth="lg">{!offline ? <List>{torrents && torrents.map((torrent) => <Torrent key={torrent.hash} torrent={torrent} />)}</List> : <Typography>Offline</Typography>}</Container>
</React.Fragment>
)
}
function getTorrentList(callback) {
fetch(torrentsHost(), {
method: 'post',
body: JSON.stringify({ action: 'list' }),
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then(
(json) => {
callback(json)
},
(error) => {
callback(null)
}
)
}

View File

@@ -0,0 +1,40 @@
import React from 'react'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import ListItem from '@material-ui/core/ListItem'
import PublishIcon from '@material-ui/icons/Publish'
import { torrentUploadHost } from '../utils/Hosts'
const classes = {
input: {
display: 'none',
},
}
export default function UploadDialog() {
const handleCapture = ({ target }) => {
let data = new FormData()
data.append('save', 'true')
for (let i = 0; i < target.files.length; i++) {
data.append('file' + i, target.files[i])
}
fetch(torrentUploadHost(), {
method: 'POST',
body: data,
})
}
return (
<div>
<input onChange={handleCapture} accept="*/*" type="file" className={classes.input} style={{ display: 'none' }} id="raised-button-file" multiple />
<label htmlFor="raised-button-file">
<ListItem button variant="raised" type="submit" component="span" className={classes.button} key="Upload file">
<ListItemIcon>
<PublishIcon />
</ListItemIcon>
<ListItemText primary="Upload file" />
</ListItem>
</label>
</div>
)
}