Merge pull request #51 from marcopeocchi/50-request-for-download-link-and-option-to-delete-downloadded-video

50 request for download link and option to delete downloadded video
This commit is contained in:
Marco
2023-05-25 11:42:01 +02:00
committed by GitHub
16 changed files with 496 additions and 46 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ downloads
.DS_Store .DS_Store
build/ build/
yt-dlp-webui yt-dlp-webui
session.dat

View File

@@ -2,7 +2,6 @@ import { ThemeProvider } from '@emotion/react'
import ChevronLeft from '@mui/icons-material/ChevronLeft' import ChevronLeft from '@mui/icons-material/ChevronLeft'
import Dashboard from '@mui/icons-material/Dashboard' import Dashboard from '@mui/icons-material/Dashboard'
import FormatListBulleted from '@mui/icons-material/FormatListBulleted'
import Menu from '@mui/icons-material/Menu' import Menu from '@mui/icons-material/Menu'
import SettingsIcon from '@mui/icons-material/Settings' import SettingsIcon from '@mui/icons-material/Settings'
import SettingsEthernet from '@mui/icons-material/SettingsEthernet' import SettingsEthernet from '@mui/icons-material/SettingsEthernet'
@@ -15,24 +14,25 @@ import CssBaseline from '@mui/material/CssBaseline'
import Divider from '@mui/material/Divider' import Divider from '@mui/material/Divider'
import IconButton from '@mui/material/IconButton' import IconButton from '@mui/material/IconButton'
import List from '@mui/material/List' import List from '@mui/material/List'
import ListItemButton from '@mui/material/ListItemButton'
import ListItemIcon from '@mui/material/ListItemIcon' import ListItemIcon from '@mui/material/ListItemIcon'
import ListItemText from '@mui/material/ListItemText' import ListItemText from '@mui/material/ListItemText'
import Toolbar from '@mui/material/Toolbar' import Toolbar from '@mui/material/Toolbar'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import DownloadIcon from '@mui/icons-material/Download';
import ListItemButton from '@mui/material/ListItemButton'
import { grey } from '@mui/material/colors' import { grey } from '@mui/material/colors'
import { Suspense, lazy, useMemo, useState } from 'react' import { Suspense, lazy, useMemo, useState } from 'react'
import { Provider, useDispatch, useSelector } from 'react-redux' import { Provider, useDispatch, useSelector } from 'react-redux'
import {
Link, Route, import { BrowserRouter, Link, Route, Routes } from 'react-router-dom'
BrowserRouter as Router, import { RootState, store } from './stores/store'
Routes
} from 'react-router-dom'
import AppBar from './components/AppBar' import AppBar from './components/AppBar'
import Drawer from './components/Drawer' import Drawer from './components/Drawer'
import { toggleListView } from './features/settings/settingsSlice'
import { RootState, store } from './stores/store' import Archive from './Archive'
import { formatGiB } from './utils' import { formatGiB } from './utils'
function AppContent() { function AppContent() {
@@ -40,7 +40,6 @@ function AppContent() {
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)
const dispatch = useDispatch()
const mode = settings.theme const mode = settings.theme
const theme = useMemo(() => const theme = useMemo(() =>
@@ -63,7 +62,7 @@ function AppContent() {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Router> <BrowserRouter>
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<CssBaseline /> <CssBaseline />
<AppBar position="absolute" open={open}> <AppBar position="absolute" open={open}>
@@ -139,12 +138,19 @@ function AppContent() {
<ListItemText primary="Home" /> <ListItemText primary="Home" />
</ListItemButton> </ListItemButton>
</Link> </Link>
<ListItemButton onClick={() => dispatch(toggleListView())}> <Link to={'/archive'} style={
<ListItemIcon> {
<FormatListBulleted /> textDecoration: 'none',
</ListItemIcon> color: mode === 'dark' ? '#ffffff' : '#000000DE'
<ListItemText primary="List view" /> }
</ListItemButton> }>
<ListItemButton disabled={status.downloading}>
<ListItemIcon>
<DownloadIcon />
</ListItemIcon>
<ListItemText primary="Archive" />
</ListItemButton>
</Link>
<Link to={'/settings'} style={ <Link to={'/settings'} style={
{ {
textDecoration: 'none', textDecoration: 'none',
@@ -180,10 +186,15 @@ function AppContent() {
<Settings /> <Settings />
</Suspense> </Suspense>
} /> } />
<Route path="/archive" element={
<Suspense fallback={<CircularProgress />}>
<Archive />
</Suspense>
} />
</Routes> </Routes>
</Box> </Box>
</Box> </Box>
</Router> </BrowserRouter>
</ThemeProvider> </ThemeProvider>
) )
} }

169
frontend/src/Archive.tsx Normal file
View File

@@ -0,0 +1,169 @@
import {
Backdrop,
Button,
Checkbox,
CircularProgress,
Container,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Paper,
SpeedDial,
SpeedDialAction,
SpeedDialIcon
} from '@mui/material'
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
import VideoFileIcon from '@mui/icons-material/VideoFile'
import { Buffer } from 'buffer'
import { useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
import { useObservable } from './hooks/observable'
import { RootState } from './stores/store'
import { DeleteRequest, DirectoryEntry } from './types'
export default function Downloaded() {
const settings = useSelector((state: RootState) => state.settings)
const [openDialog, setOpenDialog] = useState(false)
const serverAddr =
`${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}`
const files$ = useMemo(() => new Subject<DirectoryEntry[]>(), [])
const selected$ = useMemo(() => new BehaviorSubject<string[]>([]), [])
const fetcher = () => fetch(`${serverAddr}/downloaded`)
.then(res => res.json())
.then(data => files$.next(data))
const selectable$ = useMemo(() => files$.pipe(
combineLatestWith(selected$),
map(([data, selected]) => data.map(x => ({
...x,
selected: selected.includes(x.name)
}))),
share()
), [])
const selectable = useObservable(selectable$, [])
const addSelected = (name: string) => {
selected$.value.includes(name)
? selected$.next(selected$.value.filter(val => val !== name))
: selected$.next([...selected$.value, name])
}
const deleteSelected = () => {
Promise.all(selectable
.filter(entry => entry.selected)
.map(entry => fetch(`${serverAddr}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: entry.path,
shaSum: entry.shaSum,
} as DeleteRequest)
}))
).then(fetcher)
}
useEffect(() => {
fetcher()
}, [settings.serverAddr, settings.serverPort])
return (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={!(files$.observed)}
>
<CircularProgress color="primary" />
</Backdrop>
<Paper sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
}}>
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
{selectable.length === 0 && 'No files found'}
{selectable.map((file) => (
<ListItem
key={file.shaSum}
secondaryAction={
<Checkbox
edge="end"
checked={file.selected}
onChange={() => addSelected(file.name)}
/>
}
disablePadding
>
<ListItemButton>
<ListItemIcon>
<VideoFileIcon />
</ListItemIcon>
<ListItemText primary={file.name} onClick={() => window.open(
`${serverAddr}/play?path=${Buffer.from(file.path).toString('hex')}`
)} />
</ListItemButton>
</ListItem>
))}
</List>
</Paper>
<SpeedDial
ariaLabel="SpeedDial basic example"
sx={{ position: 'absolute', bottom: 32, right: 32 }}
icon={<SpeedDialIcon />}
>
<SpeedDialAction
icon={<DeleteForeverIcon />}
tooltipTitle={`Delete selected`}
tooltipOpen
onClick={() => {
if (selected$.value.length > 0) {
setOpenDialog(true)
}
}}
/>
</SpeedDial>
<Dialog
open={openDialog}
onClose={() => setOpenDialog(false)}
>
<DialogTitle>
Are you sure?
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
You're deleting:
</DialogContentText>
<ul>
{selected$.value.map((entry, idx) => (
<li key={idx}>{entry}</li>
))}
</ul>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>Cancel</Button>
<Button onClick={() => {
deleteSelected()
setOpenDialog(false)
}} autoFocus>
Ok
</Button>
</DialogActions>
</Dialog>
</Container>
)
}

View File

@@ -1,4 +1,5 @@
import { FileUpload } from '@mui/icons-material' import { FileUpload } from '@mui/icons-material'
import FormatListBulleted from '@mui/icons-material/FormatListBulleted'
import { import {
Alert, Alert,
Backdrop, Backdrop,
@@ -14,6 +15,9 @@ import {
Paper, Paper,
Select, Select,
Snackbar, Snackbar,
SpeedDial,
SpeedDialAction,
SpeedDialIcon,
styled, styled,
TextField TextField
} from '@mui/material' } from '@mui/material'
@@ -26,6 +30,7 @@ import FormatsGrid from './components/FormatsGrid'
import { CliArguments } from './features/core/argsParser' import { CliArguments } from './features/core/argsParser'
import I18nBuilder from './features/core/intl' import I18nBuilder from './features/core/intl'
import { RPCClient, socket$ } from './features/core/rpcClient' import { RPCClient, socket$ } from './features/core/rpcClient'
import { toggleListView } from './features/settings/settingsSlice'
import { connected, setFreeSpace } from './features/status/statusSlice' import { connected, setFreeSpace } from './features/status/statusSlice'
import { RootState } from './stores/store' import { RootState } from './stores/store'
import type { DLMetadata, RPCResponse, RPCResult } from './types' import type { DLMetadata, RPCResponse, RPCResult } from './types'
@@ -405,6 +410,18 @@ export default function Home() {
{`${i18n.t('rpcConnErr')} (${settings.serverAddr}:${settings.serverPort})`} {`${i18n.t('rpcConnErr')} (${settings.serverAddr}:${settings.serverPort})`}
</Alert> </Alert>
</Snackbar> </Snackbar>
<SpeedDial
ariaLabel="SpeedDial basic example"
sx={{ position: 'absolute', bottom: 32, right: 32 }}
icon={<SpeedDialIcon />}
>
<SpeedDialAction
icon={<FormatListBulleted />}
tooltipTitle={`Table view`}
tooltipOpen
onClick={() => dispatch(toggleListView())}
/>
</SpeedDial>
</Container> </Container>
); );
} }

View File

@@ -6,7 +6,7 @@ import { StackableResult } from "./StackableResult"
type Props = { type Props = {
downloads: RPCResult[] downloads: RPCResult[]
abortFunction: Function abortFunction: (id: string) => void
} }
export function DownloadsCardView({ downloads, abortFunction }: Props) { export function DownloadsCardView({ downloads, abortFunction }: Props) {

View File

@@ -1,4 +1,4 @@
import { EightK, FourK, Hd, Sd } from "@mui/icons-material"; import { EightK, FourK, Hd, Sd } from '@mui/icons-material'
import { import {
Button, Button,
Card, Card,
@@ -11,9 +11,9 @@ import {
Skeleton, Skeleton,
Stack, Stack,
Typography Typography
} from "@mui/material"; } from '@mui/material'
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
import { ellipsis, formatSpeedMiB, roundMiB } from "../utils"; import { ellipsis, formatSpeedMiB, roundMiB } from '../utils'
type Props = { type Props = {
title: string, title: string,
@@ -97,7 +97,8 @@ export function StackableResult({
variant="contained" variant="contained"
size="small" size="small"
color="primary" color="primary"
onClick={stopCallback}> onClick={stopCallback}
>
{isCompleted ? "Clear" : "Stop"} {isCompleted ? "Clear" : "Stop"}
</Button> </Button>
</CardActions> </CardActions>

View File

@@ -0,0 +1,44 @@
import { useEffect, useState } from 'react'
import { Observable } from 'rxjs'
/**
* Handles the subscription and unsubscription from an observable.
* Automatically disposes the subscription.
* @param source$ source observable
* @param nextHandler subscriber function
* @param errHandler error catching callback
*/
export function useSubscription<T>(
source$: Observable<T>,
nextHandler: (value: T) => void,
errHandler?: (err: any) => void,
) {
useEffect(() => {
if (source$) {
const sub = source$.subscribe({
next: nextHandler,
error: errHandler,
})
return () => sub.unsubscribe()
}
}, [source$])
}
/**
* Use an observable as state
* @param source$ source observable
* @param initialState the initial state prior to the emission
* @param errHandler error catching callback
* @returns value emitted to the observable
*/
export function useObservable<T>(
source$: Observable<T>,
initialState: T,
errHandler?: (err: any) => void,
): T {
const [value, setValue] = useState(initialState)
useSubscription(source$, setValue, errHandler)
return value
}

View File

@@ -10,13 +10,13 @@ export type RPCMethods =
| "Service.UpdateExecutable" | "Service.UpdateExecutable"
export type RPCRequest = { export type RPCRequest = {
method: RPCMethods, method: RPCMethods
params?: any[], params?: any[]
id?: string id?: string
} }
export type RPCResponse<T> = { export type RPCResponse<T> = {
result: T, result: T
error: number | null error: number | null
id?: string id?: string
} }
@@ -46,18 +46,29 @@ export type RPCParams = {
} }
export interface DLMetadata { export interface DLMetadata {
formats: Array<DLFormat>, formats: Array<DLFormat>
best: DLFormat, best: DLFormat
thumbnail: string, thumbnail: string
title: string, title: string
} }
export interface DLFormat { export type DLFormat = {
format_id: string, format_id: string
format_note: string, format_note: string
fps: number, fps: number
resolution: string, resolution: string
vcodec: string, vcodec: string
acodec: string, acodec: string
filesize_approx: number, filesize_approx: number
} }
export type DirectoryEntry = {
name: string
path: string
shaSum: string
}
export type DeleteRequest = Omit<DirectoryEntry, 'name'>
export type PlayRequest = Omit<DirectoryEntry, 'shaSum' | 'name'>

View File

@@ -82,6 +82,10 @@ 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 getHttpEndpoint() {
return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}`
}
export function formatGiB(bytes: number) { export function formatGiB(bytes: number) {
return `${(bytes / 1_000_000_000).toFixed(0)}GiB` return `${(bytes / 1_000_000_000).toFixed(0)}GiB`
} }

1
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/gofiber/websocket/v2 v2.1.5 github.com/gofiber/websocket/v2 v2.1.5
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc
golang.org/x/sys v0.7.0 golang.org/x/sys v0.7.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )

2
go.sum
View File

@@ -76,6 +76,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=

View File

@@ -108,4 +108,14 @@ func (m *MemoryDB) Restore() {
feed, _ := os.ReadFile("session.dat") feed, _ := os.ReadFile("session.dat")
session := Session{} session := Session{}
json.Unmarshal(feed, &session) json.Unmarshal(feed, &session)
for _, proc := range session.Processes {
db.table.Store(proc.Id, &Process{
id: proc.Id,
url: proc.Info.URL,
Info: proc.Info,
Progress: proc.Progress,
mem: m,
})
}
} }

View File

@@ -171,14 +171,17 @@ func (p *Process) Kill() error {
// has been spawned with setPgid = true. To properly kill // has been spawned with setPgid = true. To properly kill
// all subprocesses a SIGTERM need to be sent to the correct // all subprocesses a SIGTERM need to be sent to the correct
// process group // process group
pgid, err := syscall.Getpgid(p.proc.Pid) if p.proc != nil {
if err != nil { pgid, err := syscall.Getpgid(p.proc.Pid)
if err != nil {
return err
}
err = syscall.Kill(-pgid, syscall.SIGTERM)
log.Println("Killed process", p.id)
return err return err
} }
err = syscall.Kill(-pgid, syscall.SIGTERM) return nil
log.Println("Killed process", p.id)
return err
} }
// Returns the available format for this URL // Returns the available format for this URL

129
server/rest/handlers.go Normal file
View File

@@ -0,0 +1,129 @@
package rest
import (
"crypto/sha256"
"encoding/hex"
"errors"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
"golang.org/x/exp/slices"
)
type DirectoryEntry struct {
Name string `json:"name"`
Path string `json:"path"`
SHASum string `json:"shaSum"`
}
func isValidEntry(d fs.DirEntry) bool {
return !d.IsDir() &&
!strings.HasPrefix(d.Name(), ".") &&
!strings.HasSuffix(d.Name(), ".part") &&
!strings.HasSuffix(d.Name(), ".ytdl")
}
func walkDir(root string) (*[]DirectoryEntry, error) {
files := []DirectoryEntry{}
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if isValidEntry(d) {
h := sha256.New()
h.Write([]byte(path))
files = append(files, DirectoryEntry{
Path: path,
Name: d.Name(),
SHASum: hex.EncodeToString(h.Sum(nil)),
})
}
return nil
})
return &files, err
}
func ListDownloaded(ctx *fiber.Ctx) error {
root := config.Instance().GetConfig().DownloadPath
files, err := walkDir(root)
if err != nil {
return err
}
ctx.Status(http.StatusOK)
return ctx.JSON(files)
}
type DeleteRequest = DirectoryEntry
func DeleteFile(ctx *fiber.Ctx) error {
req := new(DeleteRequest)
err := ctx.BodyParser(req)
if err != nil {
return err
}
root := config.Instance().GetConfig().DownloadPath
files, err := walkDir(root)
if err != nil {
return err
}
index := slices.IndexFunc(*files, func(e DirectoryEntry) bool {
return e.Path == req.Path && e.SHASum == req.SHASum
})
if index == -1 {
ctx.SendString("shasum doesn't match")
}
if index >= 0 {
err := os.Remove(req.Path)
if err != nil {
return err
}
}
ctx.Status(fiber.StatusOK)
return ctx.JSON(index)
}
type PlayRequest struct {
Path string
}
func PlayFile(ctx *fiber.Ctx) error {
path := ctx.Query("path")
if path == "" {
return errors.New("inexistent path")
}
decoded, err := hex.DecodeString(path)
if err != nil {
return err
}
root := config.Instance().GetConfig().DownloadPath
//TODO: further path / file validations
if strings.Contains(filepath.Dir(string(decoded)), root) {
ctx.SendStatus(fiber.StatusPartialContent)
return ctx.SendFile(string(decoded))
}
ctx.Status(fiber.StatusOK)
return ctx.SendStatus(fiber.StatusUnauthorized)
}

View File

@@ -1,22 +1,30 @@
package server package server
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"log" "log"
"net/http" "net/http"
"net/rpc" "net/rpc"
"os"
"os/signal"
"syscall"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors" "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"
"github.com/marcopeocchi/yt-dlp-web-ui/server/rest"
) )
var db MemoryDB var db MemoryDB
func RunBlocking(port int, frontend fs.FS) { func RunBlocking(port int, frontend fs.FS) {
db.Restore()
service := new(Service) service := new(Service)
rpc.Register(service) rpc.Register(service)
@@ -30,6 +38,13 @@ func RunBlocking(port int, frontend fs.FS) {
app.Get("/settings", func(c *fiber.Ctx) error { app.Get("/settings", func(c *fiber.Ctx) error {
return c.Redirect("/") return c.Redirect("/")
}) })
app.Get("/archive", func(c *fiber.Ctx) error {
return c.Redirect("/")
})
app.Get("/downloaded", rest.ListDownloaded)
app.Post("/delete", rest.DeleteFile)
app.Get("/play", rest.PlayFile)
// RPC handlers // RPC handlers
// websocket // websocket
@@ -65,5 +80,34 @@ func RunBlocking(port int, frontend fs.FS) {
app.Server().StreamRequestBody = true app.Server().StreamRequestBody = true
go periodicallyPersist()
go gracefulShutdown(app)
log.Fatal(app.Listen(fmt.Sprintf(":%d", port))) log.Fatal(app.Listen(fmt.Sprintf(":%d", port)))
} }
func gracefulShutdown(app *fiber.App) {
ctx, stop := signal.NotifyContext(context.Background(),
os.Interrupt,
syscall.SIGTERM,
syscall.SIGQUIT,
)
go func() {
<-ctx.Done()
log.Println("shutdown signal received")
defer func() {
db.Persist()
stop()
app.ShutdownWithTimeout(time.Second * 5)
}()
}()
}
func periodicallyPersist() {
for {
db.Persist()
time.Sleep(time.Minute * 5)
}
}

View File

@@ -72,12 +72,15 @@ func (t *Service) Running(args NoArgs, running *Running) error {
func (t *Service) Kill(args string, killed *string) error { func (t *Service) Kill(args string, killed *string) error {
log.Println("Trying killing process with id", args) log.Println("Trying killing process with id", args)
proc, err := db.Get(args) proc, err := db.Get(args)
if err != nil { if err != nil {
return err return err
} }
if proc != nil { if proc != nil {
err = proc.Kill() err = proc.Kill()
} }
db.Delete(proc.id)
return err return err
} }