Compare commits

...

8 Commits

Author SHA1 Message Date
5a47c2d3d8 upgraded react to v19 2024-12-12 09:38:37 +01:00
f29d719df0 fixed never awaited cookies template promise 2024-12-12 09:33:09 +01:00
Marco Piovanello
6adfa71fde custom path based frontend (#231) 2024-12-05 10:00:15 +01:00
0946d374e3 more examples 2024-11-23 09:53:44 +01:00
Marco Piovanello
f68c29f838 Update README.md 2024-11-19 11:43:42 +01:00
Marco Piovanello
c46e39e736 code refactoring (#227) 2024-11-19 11:36:36 +01:00
Marco Piovanello
2885d6b5d8 Update README.md 2024-11-17 08:58:08 +01:00
Marco Piovanello
ab7932ae92 Editable templates (#225)
* editable templates

* removed unused import
2024-11-15 14:24:44 +01:00
19 changed files with 922 additions and 1188 deletions

View File

@@ -1,3 +1,7 @@
> [!NOTE]
> A poll is up to decide the future of yt-dlp-web-ui frontend! If you're interested you can take part.
> https://github.com/marcopeocchi/yt-dlp-web-ui/discussions/223
# yt-dlp Web UI # yt-dlp Web UI
A not so terrible web ui for yt-dlp. A not so terrible web ui for yt-dlp.
@@ -151,6 +155,8 @@ Usage yt-dlp-webui:
session file path (default ".") session file path (default ".")
-user string -user string
Username required for auth Username required for auth
-web string
frontend web resources path
``` ```
### Config file ### Config file
@@ -180,7 +186,7 @@ password: my_random_secret
queue_size: 4 # min. 2 queue_size: 4 # min. 2
# [optional] Full path to the yt-dlp (default: "yt-dlp") # [optional] Full path to the yt-dlp (default: "yt-dlp")
downloaderPath: /usr/local/bin/yt-dlp #downloaderPath: /usr/local/bin/yt-dlp
# [optional] Enable file based logging with rotation (default: false) # [optional] Enable file based logging with rotation (default: false)
#enable_file_logging: false #enable_file_logging: false
@@ -193,6 +199,9 @@ downloaderPath: /usr/local/bin/yt-dlp
# [optional] Path where the sqlite database will be created/opened (default: "./local.db") # [optional] Path where the sqlite database will be created/opened (default: "./local.db")
#local_database_path #local_database_path
# [optional] Path where a custom frontend will be loaded (instead of the embedded one)
#frontend_path: ./web/solid-frontend
``` ```
### Systemd integration ### Systemd integration
@@ -258,6 +267,22 @@ It is **planned** to also expose a **gRPC** server.
For more information open an issue on GitHub and I will provide more info ASAP. For more information open an issue on GitHub and I will provide more info ASAP.
## Custom frontend
To load a custom frontend you need to specify its path either in the config file ([see config file](#config-file)) or via flags.
The frontend needs to follow this structure:
```
path/to/my/frontend
├── assets
│ ├── js-chunk-1.js (example)
│ ├── js-chunk-2.js (example)
│ ├── style.css (example)
└── index.html
```
`assets` is where the resources will be loaded.
`index.html` is the entrypoint.
## Nix ## Nix
This repo adds support for Nix(OS) in various ways through a `flake-parts` flake. This repo adds support for Nix(OS) in various ways through a `flake-parts` flake.
For more info, please refer to the [official documentation](https://nixos.org/learn/). For more info, please refer to the [official documentation](https://nixos.org/learn/).

View File

@@ -0,0 +1,27 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://app:3033;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
client_max_body_size 20000m;
proxy_connect_timeout 5000;
proxy_send_timeout 5000;
proxy_read_timeout 5000;
send_timeout 5000;
}
}

View File

@@ -0,0 +1,15 @@
services:
app:
image: marcobaobao/yt-dlp-webui
volumes:
- ./downloads:/downloads
restart: unless-stopped
nginx:
image: nginx:alpine
restart: unless-stopped
volumes:
- ./app.conf:/etc/nginx/conf.d/app.conf
depends_on:
- app
ports:
- 80:80

View File

@@ -1,6 +1,6 @@
{ {
"name": "yt-dlp-webui", "name": "yt-dlp-webui",
"version": "3.2.2", "version": "3.2.3",
"description": "Frontend compontent of yt-dlp-webui", "description": "Frontend compontent of yt-dlp-webui",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -10,30 +10,29 @@
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.4", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.0.13", "@fontsource/roboto": "^5.0.13",
"@fontsource/roboto-mono": "^5.0.18", "@fontsource/roboto-mono": "^5.0.18",
"@mui/icons-material": "^5.15.16", "@mui/icons-material": "^6.2.0",
"@mui/material": "^5.15.16", "@mui/material": "^6.2.0",
"fp-ts": "^2.16.5", "fp-ts": "^2.16.5",
"react": "^18.3.1", "react": "^19.0.0",
"react-dom": "^18.3.1", "react-dom": "^19.0.0",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"react-virtuoso": "^4.7.11", "react-virtuoso": "^4.7.11",
"jotai": "^2.10.2", "jotai": "^2.10.3",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },
"devDependencies": { "devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0", "@modyfi/vite-plugin-yaml": "^1.1.0",
"@types/node": "^20.14.2", "@types/node": "^20.14.2",
"@types/react": "^18.3.3", "@types/react": "^19.0.1",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^19.0.2",
"@types/react-helmet": "^6.1.11", "@types/react-helmet": "^6.1.11",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.7.0", "@vitejs/plugin-react-swc": "^3.7.2",
"million": "^3.1.11", "typescript": "^5.7.2",
"typescript": "^5.4.5", "vite": "^6.0.3"
"vite": "^5.2.11"
} }
} }

1668
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,8 @@ export const filenameTemplateState = atomWithStorage(
localStorage.getItem('lastFilenameTemplate') ?? '' localStorage.getItem('lastFilenameTemplate') ?? ''
) )
export const downloadTemplateState = atom<string>((get) => export const downloadTemplateState = atom<Promise<string>>(async (get) =>
`${get(customArgsState)} ${get(cookiesTemplateState)}` `${get(customArgsState)} ${await get(cookiesTemplateState)}`
.replace(/ +/g, ' ') .replace(/ +/g, ' ')
.trim() .trim()
) )

View File

@@ -2,7 +2,6 @@ import { FileUpload } from '@mui/icons-material'
import CloseIcon from '@mui/icons-material/Close' import CloseIcon from '@mui/icons-material/Close'
import { import {
Autocomplete, Autocomplete,
Backdrop,
Box, Box,
Button, Button,
Checkbox, Checkbox,
@@ -21,6 +20,7 @@ import Slide from '@mui/material/Slide'
import Toolbar from '@mui/material/Toolbar' import Toolbar from '@mui/material/Toolbar'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { TransitionProps } from '@mui/material/transitions' import { TransitionProps } from '@mui/material/transitions'
import { useAtom, useAtomValue } from 'jotai'
import { import {
FC, FC,
Suspense, Suspense,
@@ -30,18 +30,22 @@ import {
useState, useState,
useTransition useTransition
} from 'react' } from 'react'
import { customArgsState, downloadTemplateState, filenameTemplateState, savedTemplatesState } from '../atoms/downloadTemplate' import {
customArgsState,
downloadTemplateState,
filenameTemplateState,
savedTemplatesState
} from '../atoms/downloadTemplate'
import { settingsState } from '../atoms/settings' import { settingsState } from '../atoms/settings'
import { availableDownloadPathsState, connectedState } from '../atoms/status' import { availableDownloadPathsState, connectedState } from '../atoms/status'
import FormatsGrid from '../components/FormatsGrid' import FormatsGrid from '../components/FormatsGrid'
import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n' import { useI18n } from '../hooks/useI18n'
import { useRPC } from '../hooks/useRPC' import { useRPC } from '../hooks/useRPC'
import type { DLMetadata } from '../types' import type { DLMetadata } from '../types'
import { toFormatArgs } from '../utils' import { toFormatArgs } from '../utils'
import ExtraDownloadOptions from './ExtraDownloadOptions' import ExtraDownloadOptions from './ExtraDownloadOptions'
import { useToast } from '../hooks/toast'
import LoadingBackdrop from './LoadingBackdrop' import LoadingBackdrop from './LoadingBackdrop'
import { useAtom, useAtomValue } from 'jotai'
const Transition = forwardRef(function Transition( const Transition = forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {

View File

@@ -0,0 +1,67 @@
import { FC, useState } from 'react'
import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import {
Button,
Grid,
TextField
} from '@mui/material'
import { useI18n } from '../hooks/useI18n'
import { CustomTemplate } from '../types'
interface Props {
template: CustomTemplate
onChange: (template: CustomTemplate) => void
onDelete: (id: string) => void
}
const TemplateTextField: FC<Props> = ({ template, onChange, onDelete }) => {
const { i18n } = useI18n()
const [editedTemplate, setEditedTemplate] = useState(template)
return (
<Grid
container
spacing={2}
justifyContent="center"
alignItems="center"
key={template.id}
sx={{ mt: 1 }}
>
<Grid item xs={3}>
<TextField
fullWidth
label={i18n.t('templatesEditorNameLabel')}
defaultValue={template.name}
onChange={(e) => setEditedTemplate({ ...editedTemplate, name: e.target.value })}
/>
</Grid>
<Grid item xs={9}>
<TextField
fullWidth
label={i18n.t('templatesEditorContentLabel')}
defaultValue={template.content}
onChange={(e) => setEditedTemplate({ ...editedTemplate, content: e.target.value })}
InputProps={{
endAdornment: <div style={{ display: 'flex', gap: 2 }}>
<Button
variant='contained'
onClick={() => onChange(editedTemplate)}>
<EditIcon />
</Button>
<Button
variant='contained'
onClick={() => onDelete(editedTemplate.id)}>
<DeleteIcon />
</Button>
</div>
}}
/>
</Grid>
</Grid>
)
}
export default TemplateTextField

View File

@@ -1,6 +1,7 @@
import AddIcon from '@mui/icons-material/Add' import AddIcon from '@mui/icons-material/Add'
import CloseIcon from '@mui/icons-material/Close' import CloseIcon from '@mui/icons-material/Close'
import DeleteIcon from '@mui/icons-material/Delete' import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import { import {
Alert, Alert,
AppBar, AppBar,
@@ -26,6 +27,7 @@ import { useI18n } from '../hooks/useI18n'
import { ffetch } from '../lib/httpClient' import { ffetch } from '../lib/httpClient'
import { CustomTemplate } from '../types' import { CustomTemplate } from '../types'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import TemplateTextField from './TemplateTextField'
const Transition = forwardRef(function Transition( const Transition = forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
@@ -55,11 +57,11 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
useEffect(() => { useEffect(() => {
if (open) { if (open) {
getTemplates() fetchTemplates()
} }
}, [open]) }, [open])
const getTemplates = async () => { const fetchTemplates = async () => {
const task = ffetch<CustomTemplate[]>(`${serverAddr}/api/v1/template/all`) const task = ffetch<CustomTemplate[]>(`${serverAddr}/api/v1/template/all`)
const either = await task() const either = await task()
@@ -89,7 +91,7 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
(l) => pushMessage(l, 'warning'), (l) => pushMessage(l, 'warning'),
() => { () => {
pushMessage('Added template') pushMessage('Added template')
getTemplates() fetchTemplates()
setTemplateName('') setTemplateName('')
setTemplateContent('') setTemplateContent('')
} }
@@ -97,6 +99,26 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
) )
} }
const updateTemplate = async (template: CustomTemplate) => {
const task = ffetch<CustomTemplate>(`${serverAddr}/api/v1/template`, {
method: 'PATCH',
body: JSON.stringify(template)
})
const either = await task()
pipe(
either,
matchW(
(l) => pushMessage(l, 'warning'),
(r) => {
pushMessage(`Updated template ${r.name}`)
fetchTemplates()
}
)
)
}
const deleteTemplate = async (id: string) => { const deleteTemplate = async (id: string) => {
const task = ffetch<unknown>(`${serverAddr}/api/v1/template/${id}`, { const task = ffetch<unknown>(`${serverAddr}/api/v1/template/${id}`, {
method: 'DELETE', method: 'DELETE',
@@ -110,7 +132,7 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
(l) => pushMessage(l, 'warning'), (l) => pushMessage(l, 'warning'),
() => { () => {
pushMessage('Deleted template') pushMessage('Deleted template')
getTemplates() fetchTemplates()
} }
) )
) )
@@ -188,38 +210,12 @@ const TemplatesEditor: React.FC<Props> = ({ open, onClose }) => {
</Grid> </Grid>
</Grid> </Grid>
{templates.map(template => ( {templates.map(template => (
<Grid <TemplateTextField
container
spacing={2}
justifyContent="center"
alignItems="center"
key={template.id} key={template.id}
sx={{ mt: 1 }} template={template}
> onChange={updateTemplate}
<Grid item xs={3}> onDelete={deleteTemplate}
<TextField />
fullWidth
label={i18n.t('templatesEditorNameLabel')}
value={template.name}
/>
</Grid>
<Grid item xs={9}>
<TextField
fullWidth
label={i18n.t('templatesEditorContentLabel')}
value={template.content}
InputProps={{
endAdornment: <Button
variant='contained'
onClick={() => {
startTransition(() => { deleteTemplate(template.id) })
}}>
<DeleteIcon />
</Button>
}}
/>
</Grid>
</Grid>
))} ))}
</Paper> </Paper>
</Grid> </Grid>

View File

@@ -0,0 +1,33 @@
import { Button, CircularProgress } from '@mui/material'
import { useI18n } from '../hooks/useI18n'
import { useRPC } from '../hooks/useRPC'
import { useState } from 'react'
import { useToast } from '../hooks/toast'
const UpdateBinaryButton: React.FC = () => {
const { i18n } = useI18n()
const { client } = useRPC()
const { pushMessage } = useToast()
const [isLoading, setIsLoading] = useState(false)
const updateBinary = () => {
setIsLoading(true)
client
.updateExecutable()
.then(() => pushMessage(i18n.t('toastUpdated'), 'success'))
.then(() => setIsLoading(false))
}
return (
<Button
variant="contained"
endIcon={isLoading ? <CircularProgress size={16} color='secondary' /> : <></>}
onClick={updateBinary}
>
{i18n.t('updateBinButton')}
</Button>
)
}
export default UpdateBinaryButton

View File

@@ -5,6 +5,7 @@ export const useI18n = () => {
const instance = useAtomValue(i18nBuilderState) const instance = useAtomValue(i18nBuilderState)
return { return {
i18n: instance i18n: instance,
t: instance.t
} }
} }

View File

@@ -191,4 +191,11 @@ export class RPCClient {
params: [] params: []
}) })
} }
public updateExecutable() {
return this.sendHTTP({
method: 'Service.UpdateExecutable',
params: []
})
}
} }

View File

@@ -1,5 +1,4 @@
import { import {
Button,
Checkbox, Checkbox,
Container, Container,
FormControl, FormControl,
@@ -18,6 +17,7 @@ import {
Typography, Typography,
capitalize capitalize
} from '@mui/material' } from '@mui/material'
import { useAtom } from 'jotai'
import { Suspense, useEffect, useMemo, useState } from 'react' import { Suspense, useEffect, useMemo, useState } from 'react'
import { import {
Subject, Subject,
@@ -44,11 +44,10 @@ import {
themeState themeState
} from '../atoms/settings' } from '../atoms/settings'
import CookiesTextField from '../components/CookiesTextField' import CookiesTextField from '../components/CookiesTextField'
import UpdateBinaryButton from '../components/UpdateBinaryButton'
import { useToast } from '../hooks/toast' import { useToast } from '../hooks/toast'
import { useI18n } from '../hooks/useI18n' import { useI18n } from '../hooks/useI18n'
import { useRPC } from '../hooks/useRPC'
import { validateDomain, validateIP } from '../utils' import { validateDomain, validateIP } from '../utils'
import { useAtom } from 'jotai'
// NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS // NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS
export default function Settings() { export default function Settings() {
@@ -72,7 +71,6 @@ export default function Settings() {
const [invalidIP, setInvalidIP] = useState(false) const [invalidIP, setInvalidIP] = useState(false)
const { i18n } = useI18n() const { i18n } = useI18n()
const { client } = useRPC()
const { pushMessage } = useToast() const { pushMessage } = useToast()
@@ -140,13 +138,6 @@ export default function Settings() {
setTheme(event.target.value as Theme) setTheme(event.target.value as Theme)
} }
/**
* Updates yt-dlp binary via RPC
*/
const updateBinary = () => {
client.updateExecutable().then(() => pushMessage(i18n.t('toastUpdated'), 'success'))
}
return ( return (
<Container maxWidth="xl" sx={{ mt: 4, mb: 8 }}> <Container maxWidth="xl" sx={{ mt: 4, mb: 8 }}>
<Paper <Paper
@@ -352,14 +343,8 @@ export default function Settings() {
</Suspense> </Suspense>
</Grid> </Grid>
<Grid> <Grid>
<Stack direction="row"> <Stack direction="row" sx={{ pt: 2 }}>
<Button <UpdateBinaryButton />
sx={{ mr: 1, mt: 3 }}
variant="contained"
onClick={() => updateBinary()}
>
{i18n.t('updateBinButton')}
</Button>
</Stack> </Stack>
</Grid> </Grid>
</Paper> </Paper>

View File

@@ -1,12 +1,10 @@
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc'
import million from 'million/compiler'
import ViteYaml from '@modyfi/vite-plugin-yaml' import ViteYaml from '@modyfi/vite-plugin-yaml'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
export default defineConfig(() => { export default defineConfig(() => {
return { return {
plugins: [ plugins: [
million.vite({ auto: true }),
react(), react(),
ViteYaml(), ViteYaml(),
], ],

View File

@@ -23,6 +23,7 @@ var (
downloaderPath string downloaderPath string
sessionFilePath string sessionFilePath string
localDatabasePath string localDatabasePath string
frontendPath string
requireAuth bool requireAuth bool
username string username string
@@ -52,6 +53,7 @@ func init() {
flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path") flag.StringVar(&downloaderPath, "driver", "yt-dlp", "yt-dlp executable path")
flag.StringVar(&sessionFilePath, "session", ".", "session file path") flag.StringVar(&sessionFilePath, "session", ".", "session file path")
flag.StringVar(&localDatabasePath, "db", "local.db", "local database path") flag.StringVar(&localDatabasePath, "db", "local.db", "local database path")
flag.StringVar(&frontendPath, "web", "", "frontend web resources path")
flag.BoolVar(&enableFileLogging, "fl", false, "enable outputting logs to a file") flag.BoolVar(&enableFileLogging, "fl", false, "enable outputting logs to a file")
flag.StringVar(&logFile, "lf", "yt-dlp-webui.log", "set log file location") flag.StringVar(&logFile, "lf", "yt-dlp-webui.log", "set log file location")
@@ -69,6 +71,10 @@ func main() {
log.Fatalln(err) log.Fatalln(err)
} }
if frontendPath != "" {
frontend = os.DirFS(frontendPath)
}
c := config.Instance() c := config.Instance()
{ {

View File

@@ -9,26 +9,26 @@ import (
) )
type Config struct { type Config struct {
LogPath string `yaml:"log_path"` LogPath string `yaml:"log_path"`
EnableFileLogging bool `yaml:"enable_file_logging"` EnableFileLogging bool `yaml:"enable_file_logging"`
BaseURL string `yaml:"base_url"` BaseURL string `yaml:"base_url"`
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
DownloadPath string `yaml:"downloadPath"` DownloadPath string `yaml:"downloadPath"`
DownloaderPath string `yaml:"downloaderPath"` DownloaderPath string `yaml:"downloaderPath"`
RequireAuth bool `yaml:"require_auth"` RequireAuth bool `yaml:"require_auth"`
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"password"` Password string `yaml:"password"`
QueueSize int `yaml:"queue_size"` QueueSize int `yaml:"queue_size"`
LocalDatabasePath string `yaml:"local_database_path"` LocalDatabasePath string `yaml:"local_database_path"`
SessionFilePath string `yaml:"session_file_path"` SessionFilePath string `yaml:"session_file_path"`
path string path string // private
UseOpenId bool `yaml:"use_openid"` UseOpenId bool `yaml:"use_openid"`
OpenIdProviderURL string `yaml:"openid_provider_url"` OpenIdProviderURL string `yaml:"openid_provider_url"`
OpenIdClientId string `yaml:"openid_client_id"` OpenIdClientId string `yaml:"openid_client_id"`
OpenIdClientSecret string `yaml:"openid_client_secret"` OpenIdClientSecret string `yaml:"openid_client_secret"`
OpenIdRedirectURL string `yaml:"openid_redirect_url"` OpenIdRedirectURL string `yaml:"openid_redirect_url"`
FrontendPath string `yaml:"frontend_path"`
} }
var ( var (

View File

@@ -34,6 +34,7 @@ func ApplyRouter(args *ContainerArgs) func(chi.Router) {
r.Post("/cookies", h.SetCookies()) r.Post("/cookies", h.SetCookies())
r.Delete("/cookies", h.DeleteCookies()) r.Delete("/cookies", h.DeleteCookies())
r.Post("/template", h.AddTemplate()) r.Post("/template", h.AddTemplate())
r.Patch("/template", h.UpdateTemplate())
r.Get("/template/all", h.GetTemplates()) r.Get("/template/all", h.GetTemplates())
r.Delete("/template/{id}", h.DeleteTemplate()) r.Delete("/template/{id}", h.DeleteTemplate())
} }

View File

@@ -34,9 +34,9 @@ func (h *Handler) Exec() http.HandlerFunc {
return return
} }
err = json.NewEncoder(w).Encode(id) if err := json.NewEncoder(w).Encode(id); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }
@@ -61,6 +61,7 @@ func (h *Handler) ExecPlaylist() http.HandlerFunc {
if err := json.NewEncoder(w).Encode("ok"); err != nil { if err := json.NewEncoder(w).Encode("ok"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }
@@ -75,13 +76,14 @@ func (h *Handler) ExecLivestream() http.HandlerFunc {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
h.service.ExecLivestream(req) h.service.ExecLivestream(req)
err := json.NewEncoder(w).Encode("ok") if err := json.NewEncoder(w).Encode("ok"); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }
@@ -98,9 +100,9 @@ func (h *Handler) Running() http.HandlerFunc {
return return
} }
err = json.NewEncoder(w).Encode(res) if err := json.NewEncoder(w).Encode(res); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }
@@ -134,21 +136,19 @@ func (h *Handler) SetCookies() http.HandlerFunc {
req := new(internal.SetCookiesRequest) req := new(internal.SetCookiesRequest)
err := json.NewDecoder(r.Body).Decode(req) if err := json.NewDecoder(r.Body).Decode(req); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
err = h.service.SetCookies(r.Context(), req.Cookies) if err := h.service.SetCookies(r.Context(), req.Cookies); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
err = json.NewEncoder(w).Encode("ok") if err := json.NewEncoder(w).Encode("ok"); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }
@@ -157,15 +157,14 @@ func (h *Handler) DeleteCookies() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
err := h.service.SetCookies(r.Context(), "") if err := h.service.SetCookies(r.Context(), ""); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
err = json.NewEncoder(w).Encode("ok") if err := json.NewEncoder(w).Encode("ok"); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }
@@ -178,8 +177,7 @@ func (h *Handler) AddTemplate() http.HandlerFunc {
req := new(internal.CustomTemplate) req := new(internal.CustomTemplate)
err := json.NewDecoder(r.Body).Decode(req) if err := json.NewDecoder(r.Body).Decode(req); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
@@ -189,15 +187,14 @@ func (h *Handler) AddTemplate() http.HandlerFunc {
return return
} }
err = h.service.SaveTemplate(r.Context(), req) if err := h.service.SaveTemplate(r.Context(), req); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
err = json.NewEncoder(w).Encode("ok") if err := json.NewEncoder(w).Encode("ok"); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }
@@ -221,6 +218,33 @@ func (h *Handler) GetTemplates() http.HandlerFunc {
} }
} }
func (h *Handler) UpdateTemplate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
req := &internal.CustomTemplate{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
res, err := h.service.UpdateTemplate(r.Context(), req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func (h *Handler) DeleteTemplate() http.HandlerFunc { func (h *Handler) DeleteTemplate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
@@ -229,15 +253,14 @@ func (h *Handler) DeleteTemplate() http.HandlerFunc {
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
err := h.service.DeleteTemplate(r.Context(), id) if err := h.service.DeleteTemplate(r.Context(), id); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
err = json.NewEncoder(w).Encode("ok") if err := json.NewEncoder(w).Encode("ok"); err != nil {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }
@@ -266,6 +289,7 @@ func (h *Handler) GetVersion() http.HandlerFunc {
if err := json.NewEncoder(w).Encode(res); err != nil { if err := json.NewEncoder(w).Encode(res); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
} }
} }

View File

@@ -133,6 +133,22 @@ func (s *Service) GetTemplates(ctx context.Context) (*[]internal.CustomTemplate,
return &templates, nil return &templates, nil
} }
func (s *Service) UpdateTemplate(ctx context.Context, t *internal.CustomTemplate) (*internal.CustomTemplate, error) {
conn, err := s.db.Conn(ctx)
if err != nil {
return nil, err
}
defer conn.Close()
_, err = conn.ExecContext(ctx, "UPDATE templates SET name = ?, content = ? WHERE id = ?", t.Name, t.Content, t.Id)
if err != nil {
return nil, err
}
return t, nil
}
func (s *Service) DeleteTemplate(ctx context.Context, id string) error { func (s *Service) DeleteTemplate(ctx context.Context, id string) error {
conn, err := s.db.Conn(ctx) conn, err := s.db.Conn(ctx)
if err != nil { if err != nil {