Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Config validation. When loading config files we now check that specified options will work. If not, the frontend will show an error message with details on what's wrong. This applies to `gui` settings (i.e. our own ones, `beets-flask/config.yaml`) and very select ones from native beets (only those which we use directly). Hopefully, this will eventually cover all config options of beets native, but this is more of an upsream task. [#224](https://github.com/pSpitzner/beets-flask/pull/224).
- Upload Files via the WebUI. You can now drag-and-drop single files into an inbox. To upload whole albums, zip them on your host first (uploading of folders directly is not implemented, as it would require a secure context).
- New config option `gui.terminal.enabled` (default: true) [#254](https://github.com/pSpitzner/beets-flask/pull/224)


### Other (dev)
Expand Down
2 changes: 1 addition & 1 deletion backend/beets_flask/config/beets_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def reload(self, extra_yaml_path: str | Path | None = None) -> Self:

This loads the user config from yaml files after resetting to defaults.

The `extra_yaml_path` argument is mainly for testing puproses, to add a last
The `extra_yaml_path` argument is mainly for testing purposes, to add a last
yaml layer with high priority.
"""
log.debug("Resetting/Reloading config")
Expand Down
1 change: 1 addition & 0 deletions backend/beets_flask/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,5 @@ class LibrarySectionSchema:

@dataclass
class TerminalSectionSchema:
enabled: bool = True
start_path: str = "/repo"
21 changes: 19 additions & 2 deletions backend/beets_flask/server/websocket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from typing import cast

import socketio
from eyconf.validation import ConfigurationError, MultiConfigurationError

from beets_flask.config import get_config
from beets_flask.logger import log

old_on = socketio.AsyncServer.on

Expand Down Expand Up @@ -34,7 +38,20 @@ def register_socketio(app):

# Register all socketio namespaces
from .status import register_status
from .terminal import register_tmux

register_tmux()
register_status()

terminal_enabled = True
try:
terminal_enabled = get_config().data.gui.terminal.enabled
except (MultiConfigurationError, ConfigurationError):
# We don't want to let the exception propagate here as it won't reach the frontend.
log.debug("Encountered config error. Will raise on next call to get_config()")

if terminal_enabled:
log.info("Setting up Web-Terminal")
from .terminal import register_tmux

register_tmux()
else:
log.info("Web-Terminal is disabled, skipping setup")
4 changes: 2 additions & 2 deletions docker/entrypoints/entrypoint_dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ redis-cli FLUSHALL
python ./generate_types.py

# we need to run with one worker for socketio to work (but need at least threads for SSEs)
# sufficient timout for the interactive import sessions, which may take a couple of minutes
# sufficient timeout for the interactive import sessions, which may take a couple of minutes
# gunicorn --worker-class eventlet -w 1 --threads 32 --timeout 300 --bind 0.0.0.0:5001 --reload 'main:create_app()'


Expand All @@ -77,5 +77,5 @@ uvicorn beets_flask.server.app:create_app --port 5001 \



# if we need to debug the continaer without running the webserver:
# if we need to debug the container without running the webserver:
# tail -f /dev/null
9 changes: 7 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ The default is `[";", ",", "&"]`.

## Terminal

### `gui.terminal.enabled`

A boolean to enable or disable the terminal in the web interface.
By default, the terminal is enabled.

### `gui.terminal.start_path`

Specifies the path that is used when starting the terminal in the web interface.
Expand All @@ -155,8 +160,8 @@ However, the import itself is always done sequentially.
This is to ensure that the import process is not interrupted by other operations.
```

### `gui.inbox.temp_dir`
Specifies the temporary directory that is used to store files during the upload process.
### `gui.inbox.temp_dir`
Specifies the temporary directory that is used to store files during the upload process.
This is useful to ensure that files are uploaded to a filesystem that is fast and has enough
space. The default value is `/tmp/beets-flask/upload`.

Expand Down
27 changes: 17 additions & 10 deletions frontend/src/components/frontpage/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Tab, { tabClasses, TabProps } from '@mui/material/Tab';
import Tabs, { tabsClasses } from '@mui/material/Tabs';
import { createLink, LinkProps, useRouterState } from '@tanstack/react-router';

import { useConfig } from '@/api/config.ts';

export const NAVBAR_HEIGHT = {
desktop: '48px',
mobile: '74px',
Expand Down Expand Up @@ -142,6 +144,8 @@ function NavItem({ label, ...props }: StyledTabProps) {

function NavTabs() {
const theme = useTheme();
const config = useConfig();

const location = useRouterState({ select: (s) => s.location });
let basePath = location.pathname.split('/')[1];

Expand All @@ -150,18 +154,21 @@ function NavTabs() {
basePath += '/' + location.pathname.split('/')[2];
}

const navItems = [
{ label: 'Home', icon: <Home />, to: '/' as const },
{ label: 'Inbox', icon: <Inbox />, to: '/inbox' as const },
//{ label: "Session", icon: <Inbox />, to: "/sessiondraft" as const },
{ label: 'Library', icon: <Library />, to: '/library/browse' as const },
{ label: 'Search', icon: <Search />, to: '/library/search' as const },
{
const navItems: StyledTabProps[] = [
{ label: 'Home', icon: <Home />, to: '/' },
{ label: 'Inbox', icon: <Inbox />, to: '/inbox' },
//{ label: "Session", icon: <Inbox />, to: '/sessiondraft'},
{ label: 'Library', icon: <Library />, to: '/library/browse' },
{ label: 'Search', icon: <Search />, to: '/library/search' },
];

if (config.gui.terminal.enabled) {
navItems.push({
label: '',
icon: <Terminal stroke={theme.palette.primary.main} />,
to: '/terminal' as const,
},
];
to: '/terminal',
});
}

const currentIdx = navItems.findIndex((item) => item.to === '/' + basePath);
const ref = useRef<HTMLDivElement>(null);
Expand Down
32 changes: 31 additions & 1 deletion frontend/src/components/frontpage/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Terminal as xTerminal } from '@xterm/xterm';
import useSocket from '@/components/common/websocket/useSocket';

import 'node_modules/@xterm/xterm/css/xterm.css';
import { useConfig } from '@/api/config.ts';
import { Socket } from 'socket.io-client';

// match our style - this is somewhat redundant with main.css
Expand Down Expand Up @@ -139,8 +140,37 @@ export function TerminalContextProvider({
}: {
children: React.ReactNode;
}) {
const { socket, isConnected } = useSocket('terminal');
const config = useConfig();

if (!config.gui.terminal.enabled) {
const noop = () => {
console.warn(
'Terminal is not available (disabled in server config).'
);
};

return (
<TerminalContext.Provider
value={{
open: false,
toggle: noop,
resetTerm: noop,
setOpen: noop,
inputText: noop,
clearInput: noop,
socket: null,
}}
>
{children}
</TerminalContext.Provider>
);
}

return <InitTerminalContext>{children}</InitTerminalContext>;
}

function InitTerminalContext({ children }: { children: React.ReactNode }) {
const { socket, isConnected } = useSocket('terminal');
const [open, setOpen] = useState(false);
const [term, setTerm] = useState<xTerminal>();

Expand Down
18 changes: 15 additions & 3 deletions frontend/src/components/inbox/settings/actionButtonSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
ACTIONS,
DEFAULT_INBOX_FOLDER_FRONTEND_CONFIG,
InboxFolderFrontendConfig,
useConfig,
} from '@/api/config';
import { Dialog } from '@/components/common/dialogs';

Expand Down Expand Up @@ -775,11 +776,22 @@ function AddActionButton({
onAdd: (action: Action) => void;
} & ButtonProps) {
const theme = useTheme();
const config = useConfig();
const [open, setOpen] = useState(false);

const defaultActions: Array<Action> = Object.entries(ACTIONS).map(
([_, a]) => a
);
function isEnabled([actionName, _action]: [string, Action]) {
switch (actionName) {
case 'import_terminal':
return config.gui.terminal.enabled;
default:
return true;
}
}

const defaultActions: Array<Action> = Object.entries(ACTIONS)
.filter(isEnabled)
.map(([_, a]) => a);

const [action, setAction] = useState<Action>(defaultActions[0]);

return (
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pythonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export interface BeetsSchema {
}

export interface TerminalSectionSchema {
enabled: boolean;
start_path: string;
}

Expand Down
8 changes: 6 additions & 2 deletions frontend/src/routes/inbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute } from '@tanstack/react-router';

import { Action } from '@/api/config';
import { Action, useConfig } from '@/api/config';
import { inboxQueryOptions } from '@/api/inbox';
import { MatchChip, StyledChip } from '@/components/common/chips';
import { Dialog } from '@/components/common/dialogs';
Expand Down Expand Up @@ -158,6 +158,8 @@ function PageHeader({ inboxes, ...props }: { inboxes: Folder[] } & BoxProps) {
/** Description of the inbox page, shown as modal on click */
function InfoDescription() {
const theme = useTheme();
const config = useConfig();

const [open, setOpen] = useState(false);
const { data } = useSuspenseQuery(inboxQueryOptions());

Expand Down Expand Up @@ -257,7 +259,9 @@ function InfoDescription() {
<ActionInfo action="retag" />
<ActionInfo action="import_best" />
<ActionInfo action="import_bootleg" />
<ActionInfo action="import_terminal" />
{config.gui.terminal.enabled ?? (
<ActionInfo action="import_terminal" />
)}
<ActionInfo action="copy_path" />
<ActionInfo action="delete" />
<ActionInfo action="undo" />
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/routes/terminal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Alert, AlertTitle, Box } from '@mui/material';
import { createFileRoute } from '@tanstack/react-router';

import { useConfig } from '@/api/config';
import { PageWrapper } from '@/components/common/page';
import { Terminal } from '@/components/frontpage/terminal';

Expand All @@ -8,6 +10,25 @@ export const Route = createFileRoute('/terminal/')({
});

function TerminalPage() {
const config = useConfig();
if (!config.gui.terminal.enabled) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="75vh"
>
<Alert severity="warning">
<AlertTitle>Terminal Disabled</AlertTitle>
<Box>
The terminal is not enabled in the server configuration.
</Box>
</Alert>
</Box>
);
}

return (
<PageWrapper
sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}
Expand Down