mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-19 13:36:09 +05:00
add react web project
This commit is contained in:
8
web/.babelrc
Normal file
8
web/.babelrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"plugins": ["@babel/plugin-transform-react-jsx", "@babel/plugin-proposal-class-properties"],
|
||||
"env": {
|
||||
"production": {
|
||||
"presets": ["minify"]
|
||||
}
|
||||
}
|
||||
}
|
||||
3
web/.env
Normal file
3
web/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
INLINE_RUNTIME_CHUNK=false
|
||||
GENERATE_SOURCEMAP=false
|
||||
SKIP_PREFLIGHT_CHECK=true
|
||||
1
web/.eslintcache
Normal file
1
web/.eslintcache
Normal file
File diff suppressed because one or more lines are too long
23
web/.gitignore
vendored
Normal file
23
web/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
10
web/.prettierrc
Normal file
10
web/.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"printWidth": 200,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"jsxBracketSameLine": false,
|
||||
"parser": "flow",
|
||||
"semi": false
|
||||
}
|
||||
1
web/README.md
Normal file
1
web/README.md
Normal file
@@ -0,0 +1 @@
|
||||
## TorrServer web client
|
||||
File diff suppressed because one or more lines are too long
17
web/gulpfile.js
Normal file
17
web/gulpfile.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const gulp = require('gulp')
|
||||
const inlinesource = require('gulp-inline-source')
|
||||
const replace = require('gulp-replace')
|
||||
|
||||
gulp.task('default', () => {
|
||||
return gulp
|
||||
.src('./build/*.html')
|
||||
.pipe(replace('.js"></script>', '.js" inline></script>'))
|
||||
.pipe(replace('rel="stylesheet">', 'rel="stylesheet" inline>'))
|
||||
.pipe(
|
||||
inlinesource({
|
||||
compress: false,
|
||||
ignore: ['png'],
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest('./dest'))
|
||||
})
|
||||
19149
web/package-lock.json
generated
Normal file
19149
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
web/package.json
Normal file
51
web/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "torrserver_web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"fontsource-roboto": "^3.0.3",
|
||||
"material-ui-image": "^3.3.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-scripts": "4.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"build-js": "npm run build && npx gulp",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.2.3",
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.3.4",
|
||||
"@babel/plugin-transform-react-jsx": "^7.2.0",
|
||||
"babel-minify": "^0.5.0",
|
||||
"babel-preset-minify": "^0.5.0",
|
||||
"eslint": "^7.14.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-inline-source": "^4.0.0",
|
||||
"gulp-replace": "^1.0.0",
|
||||
"prettier": "2.2.1"
|
||||
}
|
||||
}
|
||||
60
web/public/index.html
Normal file
60
web/public/index.html
Normal file
File diff suppressed because one or more lines are too long
36
web/src/App.js
Normal file
36
web/src/App.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import Appbar from './components/Appbar.js'
|
||||
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core'
|
||||
|
||||
const baseTheme = createMuiTheme({
|
||||
overrides: {
|
||||
MuiCssBaseline: {
|
||||
'@global': {
|
||||
html: {
|
||||
WebkitFontSmoothing: 'auto',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#3fb57a',
|
||||
},
|
||||
secondary: {
|
||||
main: '#FFA724',
|
||||
},
|
||||
tonalOffset: 0.2,
|
||||
},
|
||||
})
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MuiThemeProvider theme={baseTheme}>
|
||||
<CssBaseline />
|
||||
<Appbar />
|
||||
</MuiThemeProvider>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
93
web/src/components/Add.js
Normal file
93
web/src/components/Add.js
Normal 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>
|
||||
)
|
||||
}
|
||||
191
web/src/components/Appbar.js
Normal file
191
web/src/components/Appbar.js
Normal 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>
|
||||
)
|
||||
}
|
||||
130
web/src/components/DialogCacheInfo.js
Normal file
130
web/src/components/DialogCacheInfo.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
95
web/src/components/DialogTorrentInfo.js
Normal file
95
web/src/components/DialogTorrentInfo.js
Normal 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)
|
||||
}
|
||||
59
web/src/components/Donate.js
Normal file
59
web/src/components/Donate.js
Normal 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>
|
||||
)
|
||||
}
|
||||
40
web/src/components/RemoveAll.js
Normal file
40
web/src/components/RemoveAll.js
Normal 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>
|
||||
)
|
||||
}
|
||||
180
web/src/components/Settings.js
Normal file
180
web/src/components/Settings.js
Normal 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
|
||||
}
|
||||
*/
|
||||
224
web/src/components/Torrent.js
Normal file
224
web/src/components/Torrent.js
Normal 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
|
||||
}]
|
||||
}
|
||||
*/
|
||||
52
web/src/components/TorrentList.js
Normal file
52
web/src/components/TorrentList.js
Normal 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
40
web/src/components/Upload.js
Normal file
40
web/src/components/Upload.js
Normal 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>
|
||||
)
|
||||
}
|
||||
13
web/src/index.css
Normal file
13
web/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
11
web/src/index.js
Normal file
11
web/src/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
16
web/src/utils/Hosts.js
Normal file
16
web/src/utils/Hosts.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export var torrserverHost = ''
|
||||
// export var torrserverHost = 'http://127.0.0.1:8090'
|
||||
|
||||
export const torrentsHost = () => torrserverHost + '/torrents'
|
||||
export const cacheHost = () => torrserverHost + '/cache'
|
||||
export const torrentUploadHost = () => torrserverHost + '/torrent/upload'
|
||||
export const settingsHost = () => torrserverHost + '/settings'
|
||||
export const streamHost = () => torrserverHost + '/stream'
|
||||
export const shutdownHost = () => torrserverHost + '/shutdown'
|
||||
export const echoHost = () => torrserverHost + '/echo'
|
||||
export const playlistAllHost = () => torrserverHost + '/playlistall/all.m3u'
|
||||
export const playlistTorrHost = () => torrserverHost + '/stream'
|
||||
|
||||
export const setTorrServerHost = (host) => {
|
||||
torrserverHost = host
|
||||
}
|
||||
10
web/src/utils/Utils.js
Normal file
10
web/src/utils/Utils.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export function humanizeSize(size) {
|
||||
if (!size) return ''
|
||||
var i = Math.floor(Math.log(size) / Math.log(1024))
|
||||
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
|
||||
}
|
||||
|
||||
export function getPeerString(torrent) {
|
||||
if (!torrent || !torrent.connected_seeders) return '[0] 0 / 0'
|
||||
return '[' + torrent.connected_seeders + '] ' + torrent.active_peers + ' / ' + torrent.total_peers
|
||||
}
|
||||
Reference in New Issue
Block a user