converted dir tree

This commit is contained in:
2023-01-11 23:19:37 +01:00
parent 4d4582b3f7
commit 4c7faa1b46
14 changed files with 229 additions and 107 deletions

View File

@@ -23,18 +23,16 @@ import {
BrowserRouter as Router, Link, Route, BrowserRouter as Router, Link, Route,
Routes Routes
} from 'react-router-dom'; } from 'react-router-dom';
import { io } from "socket.io-client";
import ArchivedDownloads from "./Archived"; import ArchivedDownloads from "./Archived";
import { AppBar } from "./components/AppBar"; import { AppBar } from "./components/AppBar";
import { Drawer } from "./components/Drawer"; import { Drawer } from "./components/Drawer";
import Home from "./Home"; import Home from "./Home";
import Settings from "./Settings"; import Settings from "./Settings";
import { RootState, store } from './stores/store'; import { RootState, store } from './stores/store';
import { getWebSocketEndpoint } from "./utils"; import { formatGiB, getWebSocketEndpoint } from "./utils";
function AppContent() { function AppContent() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [freeDiskSpace, setFreeDiskSpace] = useState('');
const settings = useSelector((state: RootState) => state.settings) const settings = useSelector((state: RootState) => state.settings)
const status = useSelector((state: RootState) => state.status) const status = useSelector((state: RootState) => state.status)
@@ -58,11 +56,6 @@ function AppContent() {
setOpen(!open); setOpen(!open);
}; };
/* Get disk free space */
useEffect(() => {
}, [])
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Router> <Router>
@@ -96,14 +89,14 @@ function AppContent() {
yt-dlp WebUI yt-dlp WebUI
</Typography> </Typography>
{ {
freeDiskSpace ? status.freeSpace ?
<div style={{ <div style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexWrap: 'wrap', flexWrap: 'wrap',
}}> }}>
<Storage /> <Storage />
<span>&nbsp;{freeDiskSpace}&nbsp;</span> <span>&nbsp;{formatGiB(status.freeSpace)}&nbsp;</span>
</div> </div>
: null : null
} }
@@ -145,20 +138,6 @@ function AppContent() {
<ListItemText primary="Home" /> <ListItemText primary="Home" />
</ListItemButton> </ListItemButton>
</Link> </Link>
{/* Next release: list downloaded files */}
{/* <Link to={'/downloaded'} style={
{
textDecoration: 'none',
color: mode === 'dark' ? '#ffffff' : '#000000DE'
}
}>
<ListItemButton disabled={status.downloading}>
<ListItemIcon>
<Download />
</ListItemIcon>
<ListItemText primary="Downloaded" />
</ListItemButton>
</Link> */}
<Link to={'/settings'} style={ <Link to={'/settings'} style={
{ {
textDecoration: 'none', textDecoration: 'none',

View File

@@ -24,7 +24,7 @@ import { useDispatch, useSelector } from "react-redux";
import { CliArguments } from "./classes"; import { CliArguments } from "./classes";
import { StackableResult } from "./components/StackableResult"; import { StackableResult } from "./components/StackableResult";
import { serverStates } from "./events"; import { serverStates } from "./events";
import { connected } from "./features/status/statusSlice"; import { connected, setFreeSpace } from "./features/status/statusSlice";
import { I18nBuilder } from "./i18n"; import { I18nBuilder } from "./i18n";
import { IDLMetadata, IMessage } from "./interfaces"; import { IDLMetadata, IMessage } from "./interfaces";
import { RPCClient } from "./rpcClient"; import { RPCClient } from "./rpcClient";
@@ -73,10 +73,6 @@ export default function Home({ socket }: Props) {
useEffect(() => { useEffect(() => {
socket.onopen = () => { socket.onopen = () => {
dispatch(connected()) dispatch(connected())
console.log('oke')
socket.send('fetch-jobs')
socket.send('disk-space')
socket.send('retrieve-jobs')
} }
}, []) }, [])
@@ -85,6 +81,11 @@ export default function Home({ socket }: Props) {
return () => clearInterval(interval) return () => clearInterval(interval)
}, []) }, [])
useEffect(() => {
client.freeSpace()
.then(bytes => dispatch(setFreeSpace(bytes.result)))
}, [])
useEffect(() => { useEffect(() => {
socket.onmessage = (event) => { socket.onmessage = (event) => {
const res = client.decode(event.data) const res = client.decode(event.data)
@@ -106,10 +107,9 @@ export default function Home({ socket }: Props) {
}, []) }, [])
useEffect(() => { useEffect(() => {
fetch(`${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}/tree`) client.directoryTree()
.then(res => res.json())
.then(data => { .then(data => {
setAvailableDownloadPaths(data.flat) setAvailableDownloadPaths(data.result)
}) })
}, []) }, [])
@@ -150,10 +150,15 @@ export default function Home({ socket }: Props) {
setPickedVideoFormat('') setPickedVideoFormat('')
setPickedBestFormat('') setPickedBestFormat('')
setTimeout(() => { setShowBackdrop(true)
resetInput()
setShowBackdrop(true) client.formats(url)
}, 250) ?.then(formats => {
console.log(formats)
setDownloadFormats(formats.result)
setShowBackdrop(false)
resetInput()
})
} }
/** /**

View File

@@ -4,73 +4,73 @@ import { IMessage } from "../interfaces";
import { ellipsis } from "../utils"; import { ellipsis } from "../utils";
type Props = { type Props = {
title: string, title: string,
thumbnail: string, thumbnail: string,
resolution: string resolution: string
percentage: string, percentage: string,
size: number, size: number,
speed: number, speed: number,
stopCallback: VoidFunction, stopCallback: VoidFunction,
} }
export function StackableResult({ export function StackableResult({
title, title,
thumbnail, thumbnail,
resolution, resolution,
percentage, percentage,
speed, speed,
size, size,
stopCallback stopCallback
}: Props) { }: Props) {
const guessResolution = (xByY: string): any => { const guessResolution = (xByY: string): any => {
if (!xByY) return null; if (!xByY) return null;
if (xByY.includes('4320')) return (<EightK color="primary" />); if (xByY.includes('4320')) return (<EightK color="primary" />);
if (xByY.includes('2160')) return (<FourK color="primary" />); if (xByY.includes('2160')) return (<FourK color="primary" />);
if (xByY.includes('1080')) return (<Hd color="primary" />); if (xByY.includes('1080')) return (<Hd color="primary" />);
if (xByY.includes('720')) return (<Sd color="primary" />); if (xByY.includes('720')) return (<Sd color="primary" />);
return null; return null;
} }
const percentageToNumber = () => Number(percentage.replace('%', '')) const percentageToNumber = () => Number(percentage.replace('%', ''))
const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB` const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB`
return ( return (
<Card> <Card>
<CardActionArea> <CardActionArea>
{thumbnail !== '' ? {thumbnail !== '' ?
<CardMedia <CardMedia
component="img" component="img"
height={180} height={180}
image={thumbnail} image={thumbnail}
/> : /> :
<Skeleton variant="rectangular" height={180} /> <Skeleton variant="rectangular" height={180} />
} }
<CardContent> <CardContent>
{title !== '' ? {title !== '' ?
<Typography gutterBottom variant="h6" component="div"> <Typography gutterBottom variant="h6" component="div">
{ellipsis(title, 54)} {ellipsis(title, 54)}
</Typography> : </Typography> :
<Skeleton /> <Skeleton />
} }
<Stack direction="row" spacing={1} py={2}> <Stack direction="row" spacing={1} py={2}>
<Chip label={'Downloading'} color="primary" /> <Chip label={'Downloading'} color="primary" />
<Typography>{percentage}</Typography> <Typography>{percentage}</Typography>
<Typography>{speed}</Typography> <Typography>{speed}</Typography>
<Typography>{roundMB(size ?? 0)}</Typography> <Typography>{roundMB(size ?? 0)}</Typography>
{guessResolution(resolution)} {guessResolution(resolution)}
</Stack> </Stack>
{percentage ? {percentage ?
<LinearProgress variant="determinate" value={percentageToNumber()} /> : <LinearProgress variant="determinate" value={percentageToNumber()} /> :
null null
} }
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>
<CardActions> <CardActions>
<Button variant="contained" size="small" color="primary" onClick={stopCallback}> <Button variant="contained" size="small" color="primary" onClick={stopCallback}>
Stop Stop
</Button> </Button>
</CardActions> </CardActions>
</Card> </Card>
) )
} }

View File

@@ -1,30 +1,47 @@
import { createSlice } from "@reduxjs/toolkit" import { createSlice, PayloadAction } from "@reduxjs/toolkit"
export interface StatusState { export interface StatusState {
connected: boolean, connected: boolean,
updated: boolean, updated: boolean,
downloading: boolean, downloading: boolean,
freeSpace: number,
} }
const initialState: StatusState = { const initialState: StatusState = {
connected: false, connected: false,
updated: false, updated: false,
downloading: false, downloading: false,
freeSpace: 0,
} }
export const statusSlice = createSlice({ export const statusSlice = createSlice({
name: 'status', name: 'status',
initialState, initialState,
reducers: { reducers: {
connected: (state) => { state.connected = true }, connected: (state) => {
disconnected: (state) => { state.connected = false }, state.connected = true
updated: (state) => { state.updated = true }, },
alreadyUpdated: (state) => { state.updated = false }, disconnected: (state) => {
downloading: (state) => { state.downloading = true }, state.connected = false
finished: (state) => { state.downloading = false }, },
updated: (state) => {
state.updated = true
},
alreadyUpdated: (state) => {
state.updated = false
},
downloading: (state) => {
state.downloading = true
},
finished: (state) => {
state.downloading = false
},
setFreeSpace: (state, action: PayloadAction<number>) => {
state.freeSpace = action.payload
}
} }
}) })
export const { connected, disconnected, updated, alreadyUpdated, downloading, finished } = statusSlice.actions export const { connected, disconnected, updated, alreadyUpdated, downloading, finished, setFreeSpace } = statusSlice.actions
export default statusSlice.reducer export default statusSlice.reducer

View File

@@ -1,6 +1,8 @@
import type { RPCRequest, RPCResponse } from "./types" import type { RPCRequest, RPCResponse } from "./types"
import type { IDLMetadata } from './interfaces' import type { IDLMetadata } from './interfaces'
import { getHttpRPCEndpoint } from './utils'
export class RPCClient { export class RPCClient {
private socket: WebSocket private socket: WebSocket
private seq: number private seq: number
@@ -20,7 +22,7 @@ export class RPCClient {
private sendHTTP<T>(req: RPCRequest) { private sendHTTP<T>(req: RPCRequest) {
return new Promise<RPCResponse<T>>((resolve, reject) => { return new Promise<RPCResponse<T>>((resolve, reject) => {
fetch('/rpc-http', { fetch(getHttpRPCEndpoint(), {
method: 'POST', method: 'POST',
body: JSON.stringify(req) body: JSON.stringify(req)
}) })
@@ -75,6 +77,20 @@ export class RPCClient {
}) })
} }
public freeSpace() {
return this.sendHTTP<number>({
method: 'Service.FreeSpace',
params: [],
})
}
public directoryTree() {
return this.sendHTTP<string[]>({
method: 'Service.DirectoryTree',
params: [],
})
}
public decode(data: any): RPCResponse<any> { public decode(data: any): RPCResponse<any> {
return JSON.parse(data) return JSON.parse(data)
} }

View File

@@ -5,6 +5,7 @@ export type RPCMethods =
| "Service.KillAll" | "Service.KillAll"
| "Service.FreeSpace" | "Service.FreeSpace"
| "Service.Formats" | "Service.Formats"
| "Service.DirectoryTree"
export type RPCRequest = { export type RPCRequest = {
method: RPCMethods, method: RPCMethods,

View File

@@ -111,4 +111,8 @@ export function getWebSocketEndpoint() {
export function getHttpRPCEndpoint() { export function getHttpRPCEndpoint() {
return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/http-rpc` return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/http-rpc`
}
export function formatGiB(bytes: number) {
return `${(bytes / 1_000_000_000).toFixed(0)}GiB`
} }

View File

@@ -28,6 +28,7 @@
"koa-router": "^10.1.1", "koa-router": "^10.1.1",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"radash": "^10.6.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-redux": "^8.0.1", "react-redux": "^8.0.1",

7
pnpm-lock.yaml generated
View File

@@ -24,6 +24,7 @@ specifiers:
mime-types: ^2.1.35 mime-types: ^2.1.35
path-browserify: ^1.0.1 path-browserify: ^1.0.1
process: ^0.11.10 process: ^0.11.10
radash: ^10.6.0
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
react-redux: ^8.0.1 react-redux: ^8.0.1
@@ -46,6 +47,7 @@ dependencies:
koa-router: 10.1.1 koa-router: 10.1.1
koa-static: 5.0.0 koa-static: 5.0.0
mime-types: 2.1.35 mime-types: 2.1.35
radash: 10.6.0
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
react-redux: 8.0.4_5uumaiclxbdbzaqafclbf6maf4 react-redux: 8.0.4_5uumaiclxbdbzaqafclbf6maf4
@@ -1830,6 +1832,11 @@ packages:
react-is: 16.13.1 react-is: 16.13.1
dev: false dev: false
/radash/10.6.0:
resolution: {integrity: sha512-L0PD+kBVaPGxn0UO9yVLJUKUkuu7bLqroZbieecPUGuSEtByCtMedDSyw+arA8pnLtZduYTgHnMjRfN90gozpQ==}
engines: {node: '>=14.18.0'}
dev: false
/react-dom/18.2.0_react@18.2.0: /react-dom/18.2.0_react@18.2.0:
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies: peerDependencies:

37
server/internal/stack.go Normal file
View File

@@ -0,0 +1,37 @@
package internal
type Node[T any] struct {
Value T
}
type Stack[T any] struct {
Nodes []*Node[T]
count int
}
func (s *Stack[T]) Push(n *Node[T]) {
if s.count >= len(s.Nodes) {
Nodes := make([]*Node[T], len(s.Nodes)*2)
copy(Nodes, s.Nodes)
s.Nodes = Nodes
}
s.Nodes[s.count] = n
s.count++
}
func (s *Stack[T]) Pop() *Node[T] {
if s.count == 0 {
return nil
}
node := s.Nodes[s.count-1]
s.count--
return node
}
func (s *Stack[T]) IsEmpty() bool {
return s.count == 0
}
func (s *Stack[T]) IsNotEmpty() bool {
return s.count != 0
}

View File

@@ -157,7 +157,12 @@ func (p *Process) GetFormatsSync() (DownloadFormats, error) {
cmd.Wait() cmd.Wait()
info := DownloadFormats{URL: p.url} info := DownloadFormats{URL: p.url}
best := Format{}
json.Unmarshal(stdout, &info) json.Unmarshal(stdout, &info)
json.Unmarshal(stdout, &best)
info.Best = best
return info, nil return info, nil
} }

View File

@@ -10,6 +10,7 @@ import (
"net/rpc" "net/rpc"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/filesystem" "github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/websocket/v2" "github.com/gofiber/websocket/v2"
) )
@@ -29,6 +30,8 @@ func RunBlocking(ctx context.Context) {
app := fiber.New() app := fiber.New()
app.Use(cors.New())
app.Use("/", filesystem.New(filesystem.Config{ app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(fe), Root: http.FS(fe),
})) }))

View File

@@ -84,3 +84,9 @@ func (t *Service) FreeSpace(args NoArgs, free *uint64) error {
*free = freeSpace *free = freeSpace
return err return err
} }
func (t *Service) DirectoryTree(args NoArgs, tree *[]string) error {
dfsTree, err := sys.DirectoryTree("downloads")
*tree = *dfsTree
return err
}

View File

@@ -2,7 +2,9 @@ package sys
import ( import (
"os" "os"
"path/filepath"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@@ -18,3 +20,42 @@ func FreeSpace() (uint64, error) {
unix.Statfs(wd+"/downloads", &stat) unix.Statfs(wd+"/downloads", &stat)
return (stat.Bavail * uint64(stat.Bsize)), nil return (stat.Bavail * uint64(stat.Bsize)), nil
} }
func DirectoryTree(rootPath string) (*[]string, error) {
type Node struct {
path string
children []Node
}
stack := internal.Stack[Node]{
Nodes: make([]*internal.Node[Node], 5),
}
flattened := make([]string, 0)
root := Node{path: rootPath}
stack.Push(&internal.Node[Node]{
Value: root,
})
flattened = append(flattened, rootPath)
for stack.IsNotEmpty() {
current := stack.Pop().Value
children, err := os.ReadDir(current.path)
if err != nil {
return nil, err
}
for _, entry := range children {
childPath := filepath.Join(current.path, entry.Name())
childNode := Node{path: childPath}
if entry.IsDir() {
current.children = append(current.children, childNode)
stack.Push(&internal.Node[Node]{
Value: childNode,
})
flattened = append(flattened, childNode.path)
}
}
}
return &flattened, nil
}