Compare commits
1 Commits
326-securi
...
feat-persi
| Author | SHA1 | Date | |
|---|---|---|---|
| 13c23303a9 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -29,3 +29,4 @@ frontend/.yarn/install-state.gz
|
||||
livestreams.dat
|
||||
.vite/deps
|
||||
archive.txt
|
||||
web_config.yml
|
||||
|
||||
@@ -18,11 +18,12 @@
|
||||
"@mui/icons-material": "^6.2.0",
|
||||
"@mui/material": "^6.2.0",
|
||||
"fp-ts": "^2.16.5",
|
||||
"jotai": "^2.10.3",
|
||||
"jotai-cache": "^0.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-virtuoso": "^4.7.11",
|
||||
"jotai": "^2.10.3",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
12
frontend/pnpm-lock.yaml
generated
12
frontend/pnpm-lock.yaml
generated
@@ -32,6 +32,9 @@ importers:
|
||||
jotai:
|
||||
specifier: ^2.10.3
|
||||
version: 2.10.3(@types/react@19.0.1)(react@19.0.0)
|
||||
jotai-cache:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(jotai@2.10.3(@types/react@19.0.1)(react@19.0.0))
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0
|
||||
@@ -737,6 +740,11 @@ packages:
|
||||
is-core-module@2.12.1:
|
||||
resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==}
|
||||
|
||||
jotai-cache@0.5.0:
|
||||
resolution: {integrity: sha512-29pUuEfSXL7Ba6lxZmiNDARc73TspWzAzCy0jCkk2uEOnFJ6kaUBZTp/AZSwnIsh1ndfUfM9/QpbLU7uJAQL0A==}
|
||||
peerDependencies:
|
||||
jotai: '>=2.0.0'
|
||||
|
||||
jotai@2.10.3:
|
||||
resolution: {integrity: sha512-Nnf4IwrLhNfuz2JOQLI0V/AgwcpxvVy8Ec8PidIIDeRi4KCFpwTFIpHAAcU+yCgnw/oASYElq9UY0YdUUegsSA==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
@@ -1512,6 +1520,10 @@ snapshots:
|
||||
dependencies:
|
||||
has: 1.0.3
|
||||
|
||||
jotai-cache@0.5.0(jotai@2.10.3(@types/react@19.0.1)(react@19.0.0)):
|
||||
dependencies:
|
||||
jotai: 2.10.3(@types/react@19.0.1)(react@19.0.0)
|
||||
|
||||
jotai@2.10.3(@types/react@19.0.1)(react@19.0.0):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.1
|
||||
|
||||
@@ -16,13 +16,13 @@ import ListItemButton from '@mui/material/ListItemButton'
|
||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { grey } from '@mui/material/colors'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Link, Outlet } from 'react-router-dom'
|
||||
import { settingsState } from './atoms/settings'
|
||||
import AppBar from './components/AppBar'
|
||||
import { AppTitle } from './components/AppTitle'
|
||||
import Drawer from './components/Drawer'
|
||||
import Footer from './components/Footer'
|
||||
import Logout from './components/Logout'
|
||||
@@ -76,15 +76,7 @@ export default function Layout() {
|
||||
>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h6"
|
||||
color="inherit"
|
||||
noWrap
|
||||
sx={{ flexGrow: 1 }}
|
||||
>
|
||||
{settings.appTitle}
|
||||
</Typography>
|
||||
<AppTitle />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer variant="permanent" open={open}>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { getOrElse } from 'fp-ts/lib/Either'
|
||||
import { pipe } from 'fp-ts/lib/function'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithCache } from 'jotai-cache'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
import { ffetch } from '../lib/httpClient'
|
||||
import { CustomTemplate } from '../types'
|
||||
import { serverSideCookiesState, serverURL } from './settings'
|
||||
|
||||
export const cookiesTemplateState = atom<Promise<string>>(async (get) =>
|
||||
export const cookiesTemplateState = atomWithCache<Promise<string>>(async (get) =>
|
||||
await get(serverSideCookiesState)
|
||||
? '--cookies=cookies.txt'
|
||||
: ''
|
||||
@@ -22,7 +22,7 @@ export const filenameTemplateState = atomWithStorage(
|
||||
localStorage.getItem('lastFilenameTemplate') ?? ''
|
||||
)
|
||||
|
||||
export const savedTemplatesState = atom<Promise<CustomTemplate[]>>(async (get) => {
|
||||
export const savedTemplatesState = atomWithCache<Promise<CustomTemplate[]>>(async (get) => {
|
||||
const task = ffetch<CustomTemplate[]>(`${get(serverURL)}/api/v1/template/all`)
|
||||
const either = await task()
|
||||
|
||||
@@ -30,5 +30,4 @@ export const savedTemplatesState = atom<Promise<CustomTemplate[]>>(async (get) =
|
||||
either,
|
||||
getOrElse(() => new Array<CustomTemplate>())
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
import { pipe } from 'fp-ts/lib/function'
|
||||
import { matchW } from 'fp-ts/lib/TaskEither'
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
import { ffetch } from '../lib/httpClient'
|
||||
import { prefersDarkMode } from '../utils'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
import { atom } from 'jotai'
|
||||
|
||||
export const languages = [
|
||||
'catalan',
|
||||
@@ -121,7 +121,8 @@ export const appTitleState = atomWithStorage(
|
||||
export const serverAddressAndPortState = atom((get) => {
|
||||
if (get(servedFromReverseProxySubDirState)) {
|
||||
return `${get(serverAddressState)}/${get(servedFromReverseProxySubDirState)}/`
|
||||
.replaceAll('"', '') // TODO: atomWithStorage put extra double quotes on strings
|
||||
.replaceAll('"', '') // XXX: atomWithStorage uses JSON.stringify to serialize
|
||||
.replaceAll('//', '/') // which puts extra double quotes.
|
||||
}
|
||||
if (get(servedFromReverseProxyState)) {
|
||||
return `${get(serverAddressState)}`
|
||||
|
||||
33
frontend/src/components/AppTitle.tsx
Normal file
33
frontend/src/components/AppTitle.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Typography } from '@mui/material'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useEffect } from 'react'
|
||||
import { appTitleState } from '../atoms/settings'
|
||||
import useFetch from '../hooks/useFetch'
|
||||
|
||||
export const AppTitle: React.FC = () => {
|
||||
const [appTitle, setAppTitle] = useAtom(appTitleState)
|
||||
|
||||
const { data } = useFetch<{ title: string }>('/webconfig')
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.title) {
|
||||
setAppTitle(
|
||||
data.title.startsWith('"')
|
||||
? data.title.substring(1, data.title.length - 1)
|
||||
: data.title
|
||||
)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h6"
|
||||
color="inherit"
|
||||
noWrap
|
||||
sx={{ flexGrow: 1 }}
|
||||
>
|
||||
{appTitle.startsWith('"') ? appTitle.substring(1, appTitle.length - 1) : appTitle}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { TransitionProps } from '@mui/material/transitions'
|
||||
import { matchW } from 'fp-ts/lib/Either'
|
||||
import { matchW } from 'fp-ts/lib/TaskEither'
|
||||
import { pipe } from 'fp-ts/lib/function'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { forwardRef, startTransition, useState } from 'react'
|
||||
@@ -52,21 +52,16 @@ const SubscriptionsDialog: React.FC<Props> = ({ open, onClose }) => {
|
||||
|
||||
const baseURL = useAtomValue(serverURL)
|
||||
|
||||
const submit = async (sub: Omit<Subscription, 'id'>) => {
|
||||
const task = ffetch<void>(`${baseURL}/subscriptions`, {
|
||||
const submit = async (sub: Omit<Subscription, 'id'>) => pipe(
|
||||
ffetch<void>(`${baseURL}/subscriptions`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(sub)
|
||||
})
|
||||
const either = await task()
|
||||
|
||||
pipe(
|
||||
either,
|
||||
matchW(
|
||||
(l) => pushMessage(l, 'error'),
|
||||
(_) => onClose()
|
||||
)
|
||||
}),
|
||||
matchW(
|
||||
(l) => pushMessage(l, 'error'),
|
||||
(_) => onClose()
|
||||
)
|
||||
}
|
||||
)()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
||||
@@ -6,6 +6,12 @@ import { serverURL } from '../atoms/settings'
|
||||
import { ffetch } from '../lib/httpClient'
|
||||
import { useToast } from './toast'
|
||||
|
||||
/**
|
||||
* Wrapper hook for ffetch. Handles data retrieval and cancellation signals.
|
||||
* If R type is set to void it doesn't perform deserialization.
|
||||
* @param resource path of the resource. serverURL is prepended
|
||||
* @returns JSON decoded value, eventual error and refetcher as an object to destruct.
|
||||
*/
|
||||
const useFetch = <R>(resource: string) => {
|
||||
const base = useAtomValue(serverURL)
|
||||
|
||||
@@ -26,7 +32,10 @@ const useFetch = <R>(resource: string) => {
|
||||
)()
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
fetcher()
|
||||
|
||||
return () => controller.abort()
|
||||
}, [])
|
||||
|
||||
return { data, error, fetcher }
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { tryCatch } from 'fp-ts/TaskEither'
|
||||
import * as J from 'fp-ts/Json'
|
||||
import * as E from 'fp-ts/Either'
|
||||
import { pipe } from 'fp-ts/lib/function'
|
||||
|
||||
async function fetcher<T>(url: string, opt?: RequestInit): Promise<T> {
|
||||
async function fetcher(url: string, opt?: RequestInit, controller?: AbortController): Promise<string> {
|
||||
const jwt = localStorage.getItem('token')
|
||||
|
||||
if (opt && !opt.headers) {
|
||||
@@ -14,17 +17,25 @@ async function fetcher<T>(url: string, opt?: RequestInit): Promise<T> {
|
||||
headers: {
|
||||
...opt?.headers,
|
||||
'X-Authentication': jwt ?? ''
|
||||
}
|
||||
},
|
||||
signal: controller?.signal
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw await res.text()
|
||||
}
|
||||
|
||||
return res.json() as T
|
||||
return res.text()
|
||||
}
|
||||
|
||||
export const ffetch = <T>(url: string, opt?: RequestInit) => tryCatch(
|
||||
() => fetcher<T>(url, opt),
|
||||
export const ffetch = <T>(url: string, opt?: RequestInit, controller?: AbortController) => tryCatch(
|
||||
async () => pipe(
|
||||
await fetcher(url, opt, controller),
|
||||
J.parse,
|
||||
E.match(
|
||||
(l) => l as T,
|
||||
(r) => r as T
|
||||
)
|
||||
),
|
||||
(e) => `error while fetching: ${e}`
|
||||
)
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
Typography,
|
||||
capitalize
|
||||
} from '@mui/material'
|
||||
import { useAtom } from 'jotai'
|
||||
import { pipe } from 'fp-ts/lib/function'
|
||||
import { matchW } from 'fp-ts/lib/TaskEither'
|
||||
import { useAtom, useAtomValue } from 'jotai'
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Subject,
|
||||
@@ -34,9 +36,9 @@ import {
|
||||
accentState,
|
||||
accents,
|
||||
appTitleState,
|
||||
autoFileExtensionState,
|
||||
enableCustomArgsState,
|
||||
fileRenamingState,
|
||||
autoFileExtensionState,
|
||||
formatSelectionState,
|
||||
languageState,
|
||||
languages,
|
||||
@@ -45,12 +47,14 @@ import {
|
||||
servedFromReverseProxySubDirState,
|
||||
serverAddressState,
|
||||
serverPortState,
|
||||
serverURL,
|
||||
themeState
|
||||
} from '../atoms/settings'
|
||||
import CookiesTextField from '../components/CookiesTextField'
|
||||
import UpdateBinaryButton from '../components/UpdateBinaryButton'
|
||||
import { useToast } from '../hooks/toast'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { ffetch } from '../lib/httpClient'
|
||||
import Translator from '../lib/i18n'
|
||||
import { validateDomain, validateIP } from '../utils'
|
||||
|
||||
@@ -70,7 +74,7 @@ export default function Settings() {
|
||||
|
||||
const [pollingTime, setPollingTime] = useAtom(rpcPollingTimeState)
|
||||
const [language, setLanguage] = useAtom(languageState)
|
||||
const [appTitle, setApptitle] = useAtom(appTitleState)
|
||||
const [appTitle, setAppTitle] = useAtom(appTitleState)
|
||||
const [accent, setAccent] = useAtom(accentState)
|
||||
|
||||
const [theme, setTheme] = useAtom(themeState)
|
||||
@@ -81,7 +85,11 @@ export default function Settings() {
|
||||
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
// TODO: change name
|
||||
const derivedServerURL = useAtomValue(serverURL)
|
||||
|
||||
const baseURL$ = useMemo(() => new Subject<string>(), [])
|
||||
const appTitle$ = useMemo(() => new Subject<string>(), [])
|
||||
const serverAddr$ = useMemo(() => new Subject<string>(), [])
|
||||
const serverPort$ = useMemo(() => new Subject<string>(), [])
|
||||
|
||||
@@ -134,6 +142,25 @@ export default function Settings() {
|
||||
return () => sub.unsubscribe()
|
||||
}, [])
|
||||
|
||||
// TODO: refactor out of component. maybe use withAtomEffect from jotai/effect package.
|
||||
useEffect(() => {
|
||||
const sub = appTitle$
|
||||
.pipe(debounceTime(500))
|
||||
.subscribe(title => {
|
||||
pipe(
|
||||
ffetch(`${derivedServerURL}/webconfig/title`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(title)
|
||||
}),
|
||||
matchW(
|
||||
(l) => pushMessage(l, 'error'),
|
||||
(_) => setAppTitle(title)
|
||||
)
|
||||
)()
|
||||
})
|
||||
return () => sub.unsubscribe()
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Language toggler handler
|
||||
*/
|
||||
@@ -194,7 +221,7 @@ export default function Settings() {
|
||||
fullWidth
|
||||
label={i18n.t('appTitle')}
|
||||
defaultValue={appTitle}
|
||||
onChange={(e) => setApptitle(e.currentTarget.value)}
|
||||
onChange={(e) => appTitle$.next(e.target.value)}
|
||||
error={appTitle === ''}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -218,7 +245,7 @@ export default function Settings() {
|
||||
{ value: 500, label: '500 ms' },
|
||||
{ value: 750, label: '750 ms' },
|
||||
{ value: 1000, label: '1000 ms' },
|
||||
{ value: 2000, label: '2000 ms' },
|
||||
{ value: 2000, label: '2 s' },
|
||||
]}
|
||||
onChange={(_, value) => typeof value === 'number'
|
||||
? setPollingTime(value)
|
||||
@@ -367,7 +394,7 @@ export default function Settings() {
|
||||
/>
|
||||
}
|
||||
label={i18n.t('autoFileExtensionOption')}
|
||||
/>
|
||||
/>
|
||||
}
|
||||
<FormControlLabel
|
||||
control={
|
||||
|
||||
104
server/configurator/configurator.go
Normal file
104
server/configurator/configurator.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package configurator
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// A singleton holding configuration of the frontend component
|
||||
// with optional persistence on a file.
|
||||
type AppConfig struct {
|
||||
Title string `yaml:"title" json:"title"`
|
||||
BaseURL string `yaml:"base_url" json:"base_url"`
|
||||
Language string `yaml:"language" json:"language"`
|
||||
RPCPollingTime int `yaml:"rpc_polling_time" json:"rpc_polling_time"`
|
||||
}
|
||||
|
||||
type Configurator struct {
|
||||
mu sync.RWMutex
|
||||
Config AppConfig
|
||||
}
|
||||
|
||||
var (
|
||||
instance *Configurator
|
||||
instanceOnce sync.Once
|
||||
)
|
||||
|
||||
func Instance() *Configurator {
|
||||
instanceOnce.Do(func() {
|
||||
if instance == nil {
|
||||
instance = &Configurator{}
|
||||
|
||||
// TODO: move out of initialization
|
||||
err := instance.Load()
|
||||
if err != nil {
|
||||
slog.Error("failed initializating configurator", slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
func (c *Configurator) Load() error {
|
||||
fd, err := getConfigurationFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if err := yaml.NewDecoder(fd).Decode(&c.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Configurator) Persist() error {
|
||||
fd, err := getConfigurationFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if err := yaml.NewEncoder(fd).Encode(c.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Configurator) setAppConfig(ac *AppConfig) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
// TODO: better validaitons
|
||||
if ac.BaseURL != "" {
|
||||
c.Config.BaseURL = ac.BaseURL
|
||||
}
|
||||
if ac.Language != "" {
|
||||
c.Config.Language = ac.Language
|
||||
}
|
||||
if ac.Title != "" {
|
||||
c.Config.Title = ac.Title
|
||||
}
|
||||
if ac.RPCPollingTime >= 250 && ac.RPCPollingTime <= 2000 {
|
||||
c.Config.RPCPollingTime = ac.RPCPollingTime
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigurationFile() (*os.File, error) {
|
||||
fd, err := os.OpenFile(
|
||||
filepath.Join(config.Instance().Dir(), "web_config.yml"),
|
||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fd, nil
|
||||
}
|
||||
103
server/configurator/handlers.go
Normal file
103
server/configurator/handlers.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package configurator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// App configurator REST handlers
|
||||
|
||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(Instance().Config); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func SetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
var req AppConfig
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
Instance().setAppConfig(&req)
|
||||
|
||||
if err := Instance().Persist(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode("ok")
|
||||
}
|
||||
|
||||
func setAppTitle(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
var req string
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
editField(w, func(c *AppConfig) {
|
||||
if req != "" {
|
||||
c.Title = req
|
||||
}
|
||||
})
|
||||
|
||||
json.NewEncoder(w).Encode("ok")
|
||||
}
|
||||
|
||||
func setBaseURL(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
var req string
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
editField(w, func(c *AppConfig) {
|
||||
if req != "" {
|
||||
c.BaseURL = req
|
||||
}
|
||||
})
|
||||
|
||||
json.NewEncoder(w).Encode("ok")
|
||||
}
|
||||
|
||||
func editField(w http.ResponseWriter, editFunc func(c *AppConfig)) {
|
||||
editFunc(&Instance().Config)
|
||||
|
||||
if err := Instance().Persist(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyRouter() func(r chi.Router) {
|
||||
return func(r chi.Router) {
|
||||
r.Get("/", GetConfig)
|
||||
r.Post("/", SetConfig)
|
||||
r.Patch("/title", setAppTitle)
|
||||
r.Patch("/baseURL", setBaseURL)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archive"
|
||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/archiver"
|
||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/config"
|
||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/configurator"
|
||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/dbutil"
|
||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/filebrowser"
|
||||
"github.com/marcopiovanello/yt-dlp-web-ui/v3/server/internal"
|
||||
@@ -235,6 +236,9 @@ func newServer(c serverConfig) *http.Server {
|
||||
// Subscriptions
|
||||
r.Route("/subscriptions", subscription.Container(c.db, cronTaskRunner).ApplyRouter())
|
||||
|
||||
// Frontend config store
|
||||
r.Route("/webconfig", configurator.ApplyRouter())
|
||||
|
||||
return &http.Server{Handler: r}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,10 +48,7 @@ func (h *RestHandler) Delete() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user