increase request timeout, add downloadmanager stats, add sync counter in filebrowser

AuthorKonata <konata@posteo.jp>
Date
Commit8c78755372fa395b3a2b5cc2763b43c25d23b40a
Parentb979878
7 files changed, 106 insertions(+), 20 deletions(-)
▾Mserver/src/index.ts
@@ -50,7 +50,8 @@ if (!values.serve) {
process.exit(1);
}
const setup = new Elysia();
//increase timeout to not abort when listing huge folders
const setup = new Elysia({ serve: { idleTimeout: 255 } });
const paramType = t.ArrayString();
type DownloadHandler = InferHandler<typeof setup, "/download/*", { params: typeof paramType }>;
@@ -188,14 +189,17 @@ const app = setup
set.headers["Content-Type"] = fileScanResult.info.mimeType;
set.status = "OK";
if (query.transcode)
return new Response(await toAvif(await Bun.file(fileScanResult.path).bytes(), fileScanResult.info.mimeType, setContentType));
return new Response(
await toAvif(await Bun.file(fileScanResult.path).bytes(), fileScanResult.info.mimeType, setContentType),
);
//wrapping in new response discards the accept-range header, which we don't support here
return new Response(Bun.file(fileScanResult.path));
}
if ("content" in fileScanResult) {
set.headers["Content-Type"] = fileScanResult.info.mimeType;
set.status = "OK";
if (query.transcode) return new Response(await toAvif(fileScanResult.content, fileScanResult.info.mimeType, setContentType));
if (query.transcode)
return new Response(await toAvif(fileScanResult.content, fileScanResult.info.mimeType, setContentType));
return new Response(fileScanResult.content);
}
set.status = fileScanResult.info.status;
▾Mwebclient/src/components/DownloadManager.css
@@ -1,6 +1,20 @@
.downloadmanager-header {
display: flex;
gap: 1.5rem;
@media (max-width: 750px) {
gap: 1.0rem; /*allows the title to fit with the buttons at min width*/
}
& > span {
min-width: 0;
overflow: hidden;
margin-right: auto;
}
}
.downloadmanager-counters {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.downloadmanager-content {
▾Mwebclient/src/components/DownloadManager.tsx
@@ -54,6 +54,24 @@ export default function DownloadManager(props: DownloadManagerProps) {
return status !== undefined && typeof status === "object" && "status" in status;
}
const [downloading, setDownloading] = createSignal(0);
const [aborted, setAborted] = createSignal(0);
const [failed, setFailed] = createSignal(0);
createEffect(() => {
batch(() => {
setDownloading(0);
setAborted(0);
setFailed(0);
for (const key of downloadKeys()) {
const status = downloadStatusMap[key];
if (!isProgress(status)) continue;
if (typeof status.status === "number") setDownloading((prev) => prev + 1);
if (status.status === "Aborted") setAborted((prev) => prev + 1);
if (status.status === "Failed") setFailed((prev) => prev + 1);
}
});
});
return (
<dialog
style={{ height: "90%" }}
@@ -63,8 +81,7 @@ export default function DownloadManager(props: DownloadManagerProps) {
>
<div style={{ height: "100%" }}>
<div class="downloadmanager-header title">
Download
<div class="spacer" />
<span>Downloads</span>
<button
title="Retry all failed/aborted downloads"
onClick={() => {
@@ -111,6 +128,11 @@ export default function DownloadManager(props: DownloadManagerProps) {
</button>
</div>
<hr />
<div class="downloadmanager-counters">
<span>In progress: {downloading()}</span>
<span>Aborted: {aborted()}</span>
<span>Failed: {failed()}</span>
</div>
<div class="downloadmanager-content">
<For each={downloadKeys()}>
{(path) => {
▾Mwebclient/src/components/FileBrowser.css
@@ -75,12 +75,20 @@
}
& > span {
min-width: 0;
margin-right: auto;
}
}
& > .directory-item {
font-weight: bold;
cursor: pointer;
& > .download-button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
line-height: 0.5rem;
font-size: 12px;
}
}
}
}
@@ -129,7 +137,7 @@
}
}
@media (max-width: 600px) {
@media (max-width: 750px) {
.file-browser {
z-index: 1;
position: absolute;
▾Mwebclient/src/components/FileBrowser.tsx
@@ -16,7 +16,7 @@ import type { MediaFile, FileListing, Directory } from "music-server-shared/type
import type { AppOptions, PlaylistItem } from "../types";
import { formatFilename, generateUUID, joinPath, LocalStorageValues, toast } from "../utils";
import { ClosedFolderSVG, DownSVG, FilterSVG, MusicSVG, RefreshSVG, UpSVG } from "./SVG";
import { clearDirectoryCache, getCover, listFiles } from "../offline";
import { clearDirectoryCache, getCover, listFiles, storedFilesLive } from "../offline";
import { DownloadButton } from "./DownloadButton";
import { LoadingScreen } from "./LoadingScreen";
import type { FlatFileListing } from "../App";
@@ -260,6 +260,24 @@ export default function FileBrowser(props: FileBrowserProps) {
);
});
const [downloadedChildren, setDownloadedChildren] = createStore<Record<string, number>>({});
createEffect(() => {
const storedFiles = storedFilesLive();
const newChildren: Record<string, number> = {};
for (const storedFile of storedFiles) {
const pathParts = storedFile.toString().split("/");
// Update all parent directories in the path
for (let i = 1; i <= pathParts.length; i++) {
const parentPath = pathParts.slice(0, i).join("/");
if (parentPath) {
newChildren[parentPath] = (newChildren[parentPath] || 0) + 1;
}
}
}
setDownloadedChildren(reconcile(newChildren));
});
return (
<>
<dialog
@@ -368,15 +386,17 @@ export default function FileBrowser(props: FileBrowserProps) {
>
{(isDirectory && <ClosedFolderSVG forButton={false} />) || <MusicSVG />}
<span>{displayName}</span>
<div class="spacer" />
<DownloadButton
fullPath={fullPath}
item={file}
fetchFiles={props.fetchFiles}
isOffline={props.isOffline}
currentDir={props.currentDir}
options={props.options}
/>
<div class="download-button-wrapper">
{isDirectory && !props.isOffline() && downloadedChildren[fullPath]}
<DownloadButton
fullPath={fullPath}
item={file}
fetchFiles={props.fetchFiles}
isOffline={props.isOffline}
currentDir={props.currentDir}
options={props.options}
/>
</div>
<button
title="Add to Playlist"
onClick={(event) => {
▾Mwebclient/src/components/OptionsModal.tsx
@@ -58,13 +58,31 @@ export default function OptionsModal(props: OptionsModalProps) {
<div class="title">Player Options</div>
<hr />
<div class="options-page-buttons">
<button onClick={() => setOptionsPage(1)} classList={{ "page-button-active": optionsPage() === 1 }}>
<button
onClick={(e) => {
e.preventDefault();
return setOptionsPage(1);
}}
classList={{ "page-button-active": optionsPage() === 1 }}
>
1
</button>
<button onClick={() => setOptionsPage(2)} classList={{ "page-button-active": optionsPage() === 2 }}>
<button
onClick={(e) => {
e.preventDefault();
return setOptionsPage(2);
}}
classList={{ "page-button-active": optionsPage() === 2 }}
>
2
</button>
<button onClick={() => setOptionsPage(3)} classList={{ "page-button-active": optionsPage() === 3 }}>
<button
onClick={(e) => {
e.preventDefault();
return setOptionsPage(3);
}}
classList={{ "page-button-active": optionsPage() === 3 }}
>
3
</button>
</div>
▾Mwebclient/src/style.css
@@ -127,7 +127,7 @@ body,
font-weight: bold;
}
@media (min-width: 601px) {
@media (min-width: 751px) {
.filebrowser-toggle {
display: none;
}