diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 7d0151c..a484406 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -14,7 +14,7 @@ on: pull_request: branches: [ master ] schedule: - - cron : '0 1 * * *' + - cron : '0 1 * * 0' env: # Use docker.io for Docker Hub if empty diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 16e907b..71e322a 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,7 +32,7 @@ import { AppBar } from './components/AppBar' import { Drawer } from './components/Drawer' import { toggleListView } from './features/settings/settingsSlice' import { RootState, store } from './stores/store' -import { formatGiB, getWebSocketEndpoint } from './utils' +import { formatGiB } from './utils' function AppContent() { const [open, setOpen] = useState(false) @@ -41,8 +41,6 @@ function AppContent() { const status = useSelector((state: RootState) => state.status) const dispatch = useDispatch() - const socket = useMemo(() => new WebSocket(getWebSocketEndpoint()), []) - const mode = settings.theme const theme = useMemo(() => createTheme({ @@ -170,10 +168,10 @@ function AppContent() { > - } /> + } /> }> - + } /> diff --git a/frontend/src/Home.tsx b/frontend/src/Home.tsx index bbe1449..931d2b3 100644 --- a/frontend/src/Home.tsx +++ b/frontend/src/Home.tsx @@ -25,17 +25,13 @@ import { DownloadsListView } from './components/DownloadsListView' import FormatsGrid from './components/FormatsGrid' import { CliArguments } from './features/core/argsParser' import I18nBuilder from './features/core/intl' -import { RPCClient } from './features/core/rpcClient' +import { RPCClient, socket$ } from './features/core/rpcClient' import { connected, setFreeSpace } from './features/status/statusSlice' import { RootState } from './stores/store' -import type { DLMetadata, RPCResult } from './types' +import type { DLMetadata, RPCResponse, RPCResult } from './types' import { isValidURL, toFormatArgs } from './utils' -type Props = { - socket: WebSocket -} - -export default function Home({ socket }: Props) { +export default function Home() { // redux state const settings = useSelector((state: RootState) => state.settings) const status = useSelector((state: RootState) => state.status) @@ -60,9 +56,11 @@ export default function Home({ socket }: Props) { const [showBackdrop, setShowBackdrop] = useState(true) const [showToast, setShowToast] = useState(true) + const [socketHasError, setSocketHasError] = useState(false) + // memos const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language]) - const client = useMemo(() => new RPCClient(socket), [settings.serverAddr, settings.serverPort]) + const client = useMemo(() => new RPCClient(), [settings.serverAddr, settings.serverPort]) const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs]) // refs @@ -73,12 +71,19 @@ export default function Home({ socket }: Props) { /* WebSocket connect event handler*/ useEffect(() => { - socket.onopen = () => { - dispatch(connected()) - setCustomArgs(localStorage.getItem('last-input-args') ?? '') - setFilenameOverride(localStorage.getItem('last-filename-override') ?? '') - } - }, []) + const sub = socket$.subscribe({ + next: () => { + dispatch(connected()) + setCustomArgs(localStorage.getItem('last-input-args') ?? '') + setFilenameOverride(localStorage.getItem('last-filename-override') ?? '') + }, + complete: () => { + setSocketHasError(true) + setShowBackdrop(false) + }, + }) + return () => sub.unsubscribe() + }, [socket$]) useEffect(() => { if (status.connected) { @@ -93,21 +98,23 @@ export default function Home({ socket }: Props) { }, []) useEffect(() => { - socket.onmessage = (event) => { - const res = client.decode(event.data) - switch (typeof res.result) { - case 'object': - setActiveDownloads( - (res.result ?? []) - .filter((r: RPCResult) => !!r.info.url) - .sort((a: RPCResult, b: RPCResult) => a.info.title.localeCompare(b.info.title)) - ) - break - default: - break - } + if (status.connected) { + const sub = socket$.subscribe((event: RPCResponse) => { + switch (typeof event.result) { + case 'object': + setActiveDownloads( + (event.result ?? []) + .filter((r) => !!r.info.url) + .sort((a, b) => a.info.title.localeCompare(b.info.title)) + ) + break + default: + break + } + }) + return () => sub.unsubscribe() } - }, []) + }, [socket$, status.connected]) useEffect(() => { if (activeDownloads && activeDownloads.length >= 0) { @@ -122,18 +129,6 @@ export default function Home({ socket }: Props) { }) }, []) - const [socketHasError, setSocketHasError] = useState(false) - - useEffect(() => { - socket.onerror = () => { - setSocketHasError(true) - setShowBackdrop(false) - } - return () => { - socket.onerror = null - } - }, [socket]) - /* -------------------- callbacks-------------------- */ /** diff --git a/frontend/src/Settings.tsx b/frontend/src/Settings.tsx index c32fdf1..6dd71a9 100644 --- a/frontend/src/Settings.tsx +++ b/frontend/src/Settings.tsx @@ -40,7 +40,7 @@ import { updated } from './features/status/statusSlice' import { RootState } from './stores/store' import { validateDomain, validateIP } from './utils' -export default function Settings({ socket }: { socket: WebSocket }) { +export default function Settings() { const dispatch = useDispatch() const status = useSelector((state: RootState) => state.status) @@ -50,7 +50,7 @@ export default function Settings({ socket }: { socket: WebSocket }) { const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language]) - const client = useMemo(() => new RPCClient(socket), []) + const client = useMemo(() => new RPCClient(), []) const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), []) const serverAddr$ = useMemo(() => new Subject(), []) @@ -79,6 +79,7 @@ export default function Settings({ socket }: { socket: WebSocket }) { useEffect(() => { const sub = serverPort$ .pipe( + debounceTime(500), map(val => Number(val)), takeWhile(val => isFinite(val) && val <= 65535), ) diff --git a/frontend/src/components/ArchiveResult.tsx b/frontend/src/components/ArchiveResult.tsx index 2d575de..a71a65a 100644 --- a/frontend/src/components/ArchiveResult.tsx +++ b/frontend/src/components/ArchiveResult.tsx @@ -1,5 +1,12 @@ -import { Card, CardActionArea, CardContent, CardMedia, Skeleton, Typography } from "@mui/material"; -import { ellipsis } from "../utils"; +import { + Card, + CardActionArea, + CardContent, + CardMedia, + Skeleton, + Typography +} from '@mui/material' +import { ellipsis } from '../utils' type Props = { title: string, diff --git a/frontend/src/components/DownloadsCardView.tsx b/frontend/src/components/DownloadsCardView.tsx index 5239b9d..dea7d17 100644 --- a/frontend/src/components/DownloadsCardView.tsx +++ b/frontend/src/components/DownloadsCardView.tsx @@ -1,5 +1,6 @@ import { Grid } from "@mui/material" import { Fragment } from "react" + import type { RPCResult } from "../types" import { StackableResult } from "./StackableResult" diff --git a/frontend/src/components/DownloadsListView.tsx b/frontend/src/components/DownloadsListView.tsx index 79a18ee..95235ae 100644 --- a/frontend/src/components/DownloadsListView.tsx +++ b/frontend/src/components/DownloadsListView.tsx @@ -1,6 +1,18 @@ -import { Button, CircularProgress, Grid, LinearProgress, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material" -import { RPCResult } from "../types" +import { + Button, + Grid, + LinearProgress, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography +} from "@mui/material" import { ellipsis, formatSpeedMiB, roundMiB } from "../utils" +import type { RPCResult } from "../types" type Props = { downloads: RPCResult[] diff --git a/frontend/src/components/FormatsGrid.tsx b/frontend/src/components/FormatsGrid.tsx index 0b98173..18f18d0 100644 --- a/frontend/src/components/FormatsGrid.tsx +++ b/frontend/src/components/FormatsGrid.tsx @@ -1,5 +1,5 @@ import { Button, ButtonGroup, Grid, Paper, Typography } from "@mui/material" -import type { DLMetadata } from "../types" +import type { DLMetadata } from '../types' type Props = { downloadFormats: DLMetadata diff --git a/frontend/src/features/core/rpcClient.ts b/frontend/src/features/core/rpcClient.ts index ca0928f..92e0996 100644 --- a/frontend/src/features/core/rpcClient.ts +++ b/frontend/src/features/core/rpcClient.ts @@ -1,13 +1,14 @@ -import type { RPCRequest, RPCResponse, DLMetadata } from "../../types" +import type { DLMetadata, RPCRequest, RPCResponse } from '../../types' -import { getHttpRPCEndpoint } from '../../utils' +import { webSocket } from 'rxjs/webSocket' +import { getHttpRPCEndpoint, getWebSocketEndpoint } from '../../utils' + +export const socket$ = webSocket(getWebSocketEndpoint()) export class RPCClient { - private socket: WebSocket private seq: number - constructor(socket: WebSocket) { - this.socket = socket + constructor() { this.seq = 0 } @@ -16,7 +17,10 @@ export class RPCClient { } private send(req: RPCRequest) { - this.socket.send(JSON.stringify(req)) + socket$.next({ + ...req, + id: this.incrementSeq(), + }) } private async sendHTTP(req: RPCRequest) { @@ -35,7 +39,6 @@ export class RPCClient { public download(url: string, args: string, pathOverride = '', renameTo = '') { if (url) { this.send({ - id: this.incrementSeq(), method: 'Service.Exec', params: [{ URL: url.split("?list").at(0)!, @@ -50,7 +53,6 @@ export class RPCClient { public formats(url: string) { if (url) { return this.sendHTTP({ - id: this.incrementSeq(), method: 'Service.Formats', params: [{ URL: url.split("?list").at(0)!, @@ -61,7 +63,6 @@ export class RPCClient { public running() { this.send({ - id: this.incrementSeq(), method: 'Service.Running', params: [], }) @@ -101,8 +102,4 @@ export class RPCClient { params: [] }) } - - public decode(data: any): RPCResponse { - return JSON.parse(data) - } } \ No newline at end of file diff --git a/frontend/src/types.d.ts b/frontend/src/types/index.d.ts similarity index 100% rename from frontend/src/types.d.ts rename to frontend/src/types/index.d.ts diff --git a/server/rx/extensions.go b/server/rx/extensions.go index 5c5880c..54600d0 100644 --- a/server/rx/extensions.go +++ b/server/rx/extensions.go @@ -11,6 +11,7 @@ import "time" // // Debounce emits a string from the source channel only after a particular // time span determined a Go Interval +// // --A--B--CD--EFG-------|> // // -t-> |> diff --git a/server/server.go b/server/server.go index 4a262b3..12b6897 100644 --- a/server/server.go +++ b/server/server.go @@ -34,6 +34,10 @@ func RunBlocking(port int, frontend fs.FS) { // RPC handlers // websocket app.Get("/ws-rpc", websocket.New(func(c *websocket.Conn) { + c.WriteMessage(websocket.TextMessage, []byte(`{ + "status": "connected" + }`)) + for { mtype, reader, err := c.NextReader() if err != nil {