add websockets to get downloads list
This commit is contained in:
1
go.mod
1
go.mod
@@ -5,5 +5,6 @@ go 1.25.0
|
||||
require (
|
||||
github.com/tetratelabs/wazero v1.11.0 // indirect
|
||||
go.senan.xyz/taglib v0.11.1 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
)
|
||||
|
||||
2
go.sum
2
go.sum
@@ -2,5 +2,7 @@ github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbw
|
||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
||||
go.senan.xyz/taglib v0.11.1 h1:S3mO5e3HRRG0Ehw1jLUodYbAJK8TtqdOoNgqkC0D3uU=
|
||||
go.senan.xyz/taglib v0.11.1/go.mod h1:qyTl978MnGeZ/ny4d/t0ErLXxysA+39X4+SNSCk56Zs=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
|
||||
56
main.go
56
main.go
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"go.senan.xyz/taglib"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
func sendJSON(w http.ResponseWriter, data any, status int) {
|
||||
@@ -71,6 +72,42 @@ func main() {
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
wsServer := websocket.Server{
|
||||
Handler: func(c *websocket.Conn) {
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
type item struct {
|
||||
ID int `json:"id"`
|
||||
Album monochrome.AlbumInfo `json:"album"`
|
||||
Status DownloadStatus `json:"status"`
|
||||
InstalledFiles int `json:"installedFiles"`
|
||||
TotalFiles int `json:"totalFiles"`
|
||||
}
|
||||
items := []item{}
|
||||
for id, download := range downloads {
|
||||
items = append(items, item{
|
||||
ID: id,
|
||||
Album: download.Album,
|
||||
Status: download.Status,
|
||||
InstalledFiles: download.InstalledFiles,
|
||||
TotalFiles: download.TotalFiles,
|
||||
})
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(c).Encode(struct {
|
||||
Downloads []item `json:"downloads"`
|
||||
}{
|
||||
Downloads: items,
|
||||
}); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
mux.HandleFunc("GET /ws/downloads", wsServer.ServeHTTP)
|
||||
|
||||
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
if path == "/" {
|
||||
@@ -107,25 +144,6 @@ func main() {
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /downloads", func(w http.ResponseWriter, r *http.Request) {
|
||||
type item struct {
|
||||
ID int `json:"id"`
|
||||
Album monochrome.AlbumInfo `json:"album"`
|
||||
InstalledFiles int `json:"installedFiles"`
|
||||
TotalFiles int `json:"totalFiles"`
|
||||
}
|
||||
items := []item{}
|
||||
for id, download := range downloads {
|
||||
items = append(items, item{
|
||||
ID: id,
|
||||
Album: download.Album,
|
||||
InstalledFiles: download.InstalledFiles,
|
||||
TotalFiles: download.TotalFiles,
|
||||
})
|
||||
}
|
||||
sendJSON(w, items, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /download-album/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(r.PathValue("id"))
|
||||
if err != nil {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hot-toast": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
||||
26
web/pnpm-lock.yaml
generated
26
web/pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^19.2.0
|
||||
version: 19.2.4(react@19.2.4)
|
||||
react-hot-toast:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.39.1
|
||||
@@ -742,6 +745,11 @@ packages:
|
||||
resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
goober@2.1.18:
|
||||
resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==}
|
||||
peerDependencies:
|
||||
csstype: ^3.0.10
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
@@ -975,6 +983,13 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^19.2.4
|
||||
|
||||
react-hot-toast@2.6.0:
|
||||
resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: '>=16'
|
||||
react-dom: '>=16'
|
||||
|
||||
react-refresh@0.18.0:
|
||||
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1836,6 +1851,10 @@ snapshots:
|
||||
|
||||
globals@16.5.0: {}
|
||||
|
||||
goober@2.1.18(csstype@3.2.3):
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
@@ -2015,6 +2034,13 @@ snapshots:
|
||||
react: 19.2.4
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-hot-toast@2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
goober: 2.1.18(csstype@3.2.3)
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
react-refresh@0.18.0: {}
|
||||
|
||||
react@19.2.4: {}
|
||||
|
||||
@@ -1,27 +1,62 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchQuery, type Album } from "./api/useSearchQuery";
|
||||
import { useDebouncedValue } from "./hooks/useDebouncedValue";
|
||||
import { getContrastYIQ } from "./getContrastYIQ";
|
||||
import { useDownloadAlbumMutation } from "./api/useDownloadAlbumMutation";
|
||||
import { useDownloadsQuery } from "./api/useDownloadsQuery";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
type DownloadItem = {
|
||||
id: number;
|
||||
status: "ADDED" | "IN_PROGRESS" | "COMPLETED";
|
||||
album: Album;
|
||||
installedFiles: number;
|
||||
totalFiles: number;
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebouncedValue(search);
|
||||
|
||||
const [downloads, setDownloads] = useState<DownloadItem[]>([]);
|
||||
|
||||
const { data } = useSearchQuery({
|
||||
query: debouncedSearch,
|
||||
});
|
||||
const { data: downloads } = useDownloadsQuery();
|
||||
|
||||
const downloadAlbumMutation = useDownloadAlbumMutation();
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket("/ws/downloads");
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("ws connection opened");
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("ws connection closed");
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data) as { downloads: DownloadItem[] };
|
||||
const items = [...data.downloads].sort((a, b) => {
|
||||
if (a.status === "COMPLETED" && b.status !== "COMPLETED") {
|
||||
return -1;
|
||||
}
|
||||
if (a.status !== "COMPLETED" && b.status === "COMPLETED") {
|
||||
return 1;
|
||||
}
|
||||
return a.album.title.localeCompare(b.album.title);
|
||||
});
|
||||
setDownloads(items);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDownloadAlbum = (album: Album) => {
|
||||
downloadAlbumMutation.mutate(
|
||||
{ albumId: album.id },
|
||||
{
|
||||
onError(error) {
|
||||
console.log({ error });
|
||||
toast.error(error.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -42,6 +77,7 @@ export default function App() {
|
||||
/>
|
||||
<div>
|
||||
<p>{item.album.title}</p>
|
||||
<p>Status: {formatDownloadStatus(item.status)}</p>
|
||||
<p>
|
||||
Downloaded {item.installedFiles}/{item.totalFiles}
|
||||
</p>
|
||||
@@ -102,3 +138,14 @@ export default function App() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const formatDownloadStatus = (status: DownloadItem["status"]): string => {
|
||||
switch (status) {
|
||||
case "ADDED":
|
||||
return "added";
|
||||
case "IN_PROGRESS":
|
||||
return "in progress";
|
||||
case "COMPLETED":
|
||||
return "completed";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Album } from "./useSearchQuery";
|
||||
|
||||
export type DownloadItem = {
|
||||
id: number;
|
||||
album: Album;
|
||||
installedFiles: number;
|
||||
totalFiles: number;
|
||||
};
|
||||
|
||||
export const useDownloadsQuery = () => {
|
||||
return useQuery({
|
||||
queryKey: ["downloads"],
|
||||
queryFn: async () => {
|
||||
const resp = await fetch("/downloads");
|
||||
return (await resp.json()) as DownloadItem[];
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -15,6 +16,7 @@ const queryClient = new QueryClient({
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Toaster />
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
|
||||
Reference in New Issue
Block a user