10 playlist download (#71)

* leveraging message queue for playlist entries DL

* playlist support implemented

It's a little bit slow but solid enough :D
This commit is contained in:
Marco
2023-07-28 11:44:38 +02:00
committed by GitHub
parent d4f656fd87
commit 68c829c40e
15 changed files with 257 additions and 58 deletions

View File

@@ -32,6 +32,7 @@ languages:
splashText: No active downloads splashText: No active downloads
archiveTitle: Archive archiveTitle: Archive
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may close this window)
italian: italian:
urlInput: URL di YouTube o di qualsiasi altro servizio supportato urlInput: URL di YouTube o di qualsiasi altro servizio supportato
statusTitle: Stato statusTitle: Stato
@@ -63,6 +64,7 @@ languages:
splashText: Nessun download attivo splashText: Nessun download attivo
archiveTitle: Archivio archiveTitle: Archivio
clipboardAction: URL copiato negli appunti clipboardAction: URL copiato negli appunti
playlistCheckbox: Download playlist (richiederà tempo, puoi chiudere la finestra dopo l'inoltro)
chinese: chinese:
urlInput: YouTube 或其他受支持服务的视频网址 urlInput: YouTube 或其他受支持服务的视频网址
statusTitle: 状态 statusTitle: 状态
@@ -95,6 +97,7 @@ languages:
splashText: 没有正在进行的下载 splashText: 没有正在进行的下载
archiveTitle: 归档 archiveTitle: 归档
clipboardAction: 复制 URL 到剪贴板 clipboardAction: 复制 URL 到剪贴板
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
spanish: spanish:
urlInput: URL de YouTube u otro servicio compatible urlInput: URL de YouTube u otro servicio compatible
statusTitle: Estado statusTitle: Estado
@@ -126,6 +129,7 @@ languages:
splashText: No active downloads splashText: No active downloads
archiveTitle: Archive archiveTitle: Archive
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
russian: russian:
urlInput: URL-адрес YouTube или любого другого поддерживаемого сервиса urlInput: URL-адрес YouTube или любого другого поддерживаемого сервиса
statusTitle: Статус statusTitle: Статус
@@ -157,6 +161,7 @@ languages:
splashText: Нет активных загрузок splashText: Нет активных загрузок
archiveTitle: Архив archiveTitle: Архив
clipboardAction: URL скопирован в буфер обмена clipboardAction: URL скопирован в буфер обмена
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
korean: korean:
urlInput: YouTube나 다른 지원되는 사이트의 URL urlInput: YouTube나 다른 지원되는 사이트의 URL
statusTitle: 상태 statusTitle: 상태
@@ -188,6 +193,7 @@ languages:
splashText: No active downloads splashText: No active downloads
archiveTitle: Archive archiveTitle: Archive
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
japanese: japanese:
urlInput: YouTubeまたはサポート済み動画のURL urlInput: YouTubeまたはサポート済み動画のURL
statusTitle: 状態 statusTitle: 状態
@@ -220,6 +226,7 @@ languages:
splashText: No active downloads splashText: No active downloads
archiveTitle: Archive archiveTitle: Archive
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
catalan: catalan:
urlInput: URL de YouTube o d'un altre servei compatible urlInput: URL de YouTube o d'un altre servei compatible
statusTitle: Estat statusTitle: Estat
@@ -251,6 +258,7 @@ languages:
splashText: No active downloads splashText: No active downloads
archiveTitle: Archive archiveTitle: Archive
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
ukrainian: ukrainian:
urlInput: URL-адреса YouTube або будь-якого іншого підтримуваного сервісу urlInput: URL-адреса YouTube або будь-якого іншого підтримуваного сервісу
statusTitle: Статус statusTitle: Статус
@@ -282,6 +290,7 @@ languages:
splashText: Немає активних завантажень splashText: Немає активних завантажень
archiveTitle: Архів archiveTitle: Архів
clipboardAction: URL скопійовано в буфер обміну clipboardAction: URL скопійовано в буфер обміну
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)
polish: polish:
urlInput: Adres URL YouTube lub innej obsługiwanej usługi urlInput: Adres URL YouTube lub innej obsługiwanej usługi
statusTitle: Status statusTitle: Status
@@ -313,3 +322,4 @@ languages:
splashText: Brak aktywnych pobrań splashText: Brak aktywnych pobrań
archiveTitle: Archiwum archiveTitle: Archiwum
clipboardAction: Adres URL zostanie skopiowany do schowka clipboardAction: Adres URL zostanie skopiowany do schowka
playlistCheckbox: Download playlist (it will take time, after submitting you may even close this window)

View File

@@ -3,8 +3,10 @@ import CloseIcon from '@mui/icons-material/Close'
import { import {
Backdrop, Backdrop,
Button, Button,
Checkbox,
Container, Container,
FormControl, FormControl,
FormControlLabel,
Grid, Grid,
IconButton, IconButton,
InputAdornment, InputAdornment,
@@ -12,15 +14,15 @@ import {
MenuItem, MenuItem,
Paper, Paper,
Select, Select,
styled, TextField,
TextField styled
} from '@mui/material' } from '@mui/material'
import AppBar from '@mui/material/AppBar' import AppBar from '@mui/material/AppBar'
import Dialog from '@mui/material/Dialog' import Dialog from '@mui/material/Dialog'
import Slide from '@mui/material/Slide' import Slide from '@mui/material/Slide'
import Toolbar from '@mui/material/Toolbar' import Toolbar from '@mui/material/Toolbar'
import { TransitionProps } from '@mui/material/transitions'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { TransitionProps } from '@mui/material/transitions'
import { Buffer } from 'buffer' import { Buffer } from 'buffer'
import { import {
forwardRef, forwardRef,
@@ -79,6 +81,8 @@ export default function DownloadDialog({
const [url, setUrl] = useState('') const [url, setUrl] = useState('')
const [workingUrl, setWorkingUrl] = useState('') const [workingUrl, setWorkingUrl] = useState('')
const [isPlaylist, setIsPlaylist] = useState(false)
// memos // memos
const cliArgs = useMemo(() => const cliArgs = useMemo(() =>
new CliArguments().fromString(settings.cliArgs), [settings.cliArgs]) new CliArguments().fromString(settings.cliArgs), [settings.cliArgs])
@@ -120,7 +124,8 @@ export default function DownloadDialog({
immediate || url || workingUrl, immediate || url || workingUrl,
`${cliArgs.toString()} ${toFormatArgs(codes)} ${customArgs}`, `${cliArgs.toString()} ${toFormatArgs(codes)} ${customArgs}`,
availableDownloadPaths[downloadPath] ?? '', availableDownloadPaths[downloadPath] ?? '',
fileNameOverride fileNameOverride,
isPlaylist,
) )
setUrl('') setUrl('')
@@ -323,7 +328,7 @@ export default function DownloadDialog({
</Grid> </Grid>
} }
</Grid> </Grid>
<Grid container spacing={1} pt={2}> <Grid container spacing={1} pt={2} justifyContent="space-between">
<Grid item> <Grid item>
<Button <Button
variant="contained" variant="contained"
@@ -336,6 +341,13 @@ export default function DownloadDialog({
{settings.formatSelection ? i18n.t('selectFormatButton') : i18n.t('startButton')} {settings.formatSelection ? i18n.t('selectFormatButton') : i18n.t('startButton')}
</Button> </Button>
</Grid> </Grid>
<Grid item>
<FormControlLabel
control={<Checkbox onChange={() => setIsPlaylist(state => !state)} />}
checked={isPlaylist}
label={i18n.t('playlistCheckbox')}
/>
</Grid>
</Grid> </Grid>
</Paper> </Paper>
</Grid> </Grid>

View File

@@ -36,18 +36,35 @@ export class RPCClient {
return data return data
} }
public download(url: string, args: string, pathOverride = '', renameTo = '') { public download(
if (url) { url: string,
this.send({ args: string,
method: 'Service.Exec', pathOverride = '',
renameTo = '',
playlist?: boolean
) {
if (!url) {
return
}
if (playlist) {
return this.send({
method: 'Service.ExecPlaylist',
params: [{ params: [{
URL: url.split("?list").at(0)!, URL: url,
Params: args.split(" ").map(a => a.trim()), Params: args.split(" ").map(a => a.trim()),
Path: pathOverride, Path: pathOverride,
Rename: renameTo,
}] }]
}) })
} }
this.send({
method: 'Service.Exec',
params: [{
URL: url.split("?list").at(0)!,
Params: args.split(" ").map(a => a.trim()),
Path: pathOverride,
Rename: renameTo,
}]
})
} }
public formats(url: string) { public formats(url: string) {

View File

@@ -6,6 +6,7 @@ export type RPCMethods =
| "Service.KillAll" | "Service.KillAll"
| "Service.FreeSpace" | "Service.FreeSpace"
| "Service.Formats" | "Service.Formats"
| "Service.ExecPlaylist"
| "Service.DirectoryTree" | "Service.DirectoryTree"
| "Service.UpdateExecutable" | "Service.UpdateExecutable"

View File

@@ -76,7 +76,13 @@ export default function Home() {
}, [status.connected]) }, [status.connected])
useEffect(() => { useEffect(() => {
client.freeSpace().then(bytes => dispatch(setFreeSpace(bytes.result))) client
.freeSpace()
.then(bytes => dispatch(setFreeSpace(bytes.result)))
.catch(() => {
setSocketHasError(true)
setShowBackdrop(false)
})
}, []) }, [])
useEffect(() => { useEffect(() => {

2
go.mod
View File

@@ -12,3 +12,5 @@ require (
golang.org/x/sys v0.9.0 golang.org/x/sys v0.9.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require github.com/go-chi/cors v1.2.1

2
go.sum
View File

@@ -1,5 +1,7 @@
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=

View File

@@ -12,15 +12,16 @@ type DownloadProgress struct {
// Used to deser the yt-dlp -J output // Used to deser the yt-dlp -J output
type DownloadInfo struct { type DownloadInfo struct {
URL string `json:"url"` URL string `json:"url"`
Title string `json:"title"` Title string `json:"title"`
Thumbnail string `json:"thumbnail"` Thumbnail string `json:"thumbnail"`
Resolution string `json:"resolution"` Resolution string `json:"resolution"`
Size int32 `json:"filesize_approx"` Size int32 `json:"filesize_approx"`
VCodec string `json:"vcodec"` VCodec string `json:"vcodec"`
ACodec string `json:"acodec"` ACodec string `json:"acodec"`
Extension string `json:"ext"` Extension string `json:"ext"`
CreatedAt time.Time `json:"created_at"` OriginalURL string `json:"original_url"`
CreatedAt time.Time `json:"created_at"`
} }
// Used to deser the formats in the -J output // Used to deser the formats in the -J output
@@ -64,11 +65,9 @@ type AbortRequest struct {
// struct representing the intent to start a download // struct representing the intent to start a download
type DownloadRequest struct { type DownloadRequest struct {
Url string `json:"url"` Id string
Params []string `json:"params"` URL string
RenameTo string `json:"renameTo"` Path string
Id string Rename string
URL string Params []string
Path string
Rename string
} }

View File

@@ -30,6 +30,7 @@ func (m *MemoryDB) Get(id string) (*Process, error) {
func (m *MemoryDB) Set(process *Process) string { func (m *MemoryDB) Set(process *Process) string {
id := uuid.Must(uuid.NewRandom()).String() id := uuid.Must(uuid.NewRandom()).String()
m.table.Store(id, process) m.table.Store(id, process)
process.Id = id
return id return id
} }
@@ -129,7 +130,6 @@ func (m *MemoryDB) Restore() {
Url: proc.Info.URL, Url: proc.Info.URL,
Info: proc.Info, Info: proc.Info,
Progress: proc.Progress, Progress: proc.Progress,
DB: m,
}) })
} }

View File

@@ -30,7 +30,15 @@ func NewMessageQueue() *MessageQueue {
// Publish a message to the queue and set the task to a peding state. // Publish a message to the queue and set the task to a peding state.
func (m *MessageQueue) Publish(p *Process) { func (m *MessageQueue) Publish(p *Process) {
go p.SetPending() p.SetPending()
go p.SetMetadata()
m.producerCh <- p
}
// Publish a message to the queue and set the task to a peding state.
// ENSURE P IS PART OF A PLAYLIST
// Needs a further review
func (m *MessageQueue) PublishPlaylistEntry(p *Process) {
m.producerCh <- p m.producerCh <- p
} }
@@ -45,3 +53,13 @@ func (m *MessageQueue) Subscriber() {
}(msg) }(msg)
} }
} }
// Empties the message queue
func (m *MessageQueue) Empty() {
for range m.producerCh {
<-m.producerCh
}
for range m.consumerCh {
<-m.consumerCh
}
}

View File

@@ -0,0 +1,81 @@
package internal
import (
"errors"
"log"
"os/exec"
"time"
"github.com/goccy/go-json"
"github.com/marcopeocchi/yt-dlp-web-ui/server/cli"
)
type metadata struct {
Entries []DownloadInfo `json:"entries"`
Count int `json:"playlist_count"`
Type string `json:"_type"`
}
func PlaylistDetect(req DownloadRequest, mq *MessageQueue, db *MemoryDB) error {
cmd := exec.Command(cfg.GetConfig().DownloaderPath, req.URL, "-J")
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
m := metadata{}
err = cmd.Start()
if err != nil {
return err
}
log.Println(cli.BgRed, "Decoding metadata", cli.Reset, req.URL)
err = json.NewDecoder(stdout).Decode(&m)
if err != nil {
return err
}
log.Println(cli.BgGreen, "Decoded metadata", cli.Reset, req.URL)
if m.Type == "" {
cmd.Wait()
return errors.New("probably not a valid URL")
}
if m.Type == "playlist" {
log.Println(
cli.BgGreen, "Playlist detected", cli.Reset, m.Count, "entries",
)
for _, meta := range m.Entries {
proc := &Process{
Url: meta.OriginalURL,
Progress: DownloadProgress{},
Output: DownloadOutput{},
Info: meta,
Params: req.Params,
}
proc.Info.URL = meta.OriginalURL
proc.Info.CreatedAt = time.Now().Add(time.Second)
db.Set(proc)
proc.SetPending()
mq.PublishPlaylistEntry(proc)
}
err = cmd.Wait()
return err
}
proc := &Process{Url: req.URL, Params: req.Params}
mq.Publish(proc)
log.Println("Sending new process to message queue", proc.Url)
err = cmd.Wait()
return err
}

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"regexp" "regexp"
"sync"
"syscall" "syscall"
"github.com/goccy/go-json" "github.com/goccy/go-json"
@@ -50,7 +51,6 @@ type Process struct {
Params []string Params []string
Info DownloadInfo Info DownloadInfo
Progress DownloadProgress Progress DownloadProgress
DB *MemoryDB
Output DownloadOutput Output DownloadOutput
proc *os.Process proc *os.Process
} }
@@ -128,7 +128,7 @@ func (p *Process) Start() {
for scan.Scan() { for scan.Scan() {
stdout := ProgressTemplate{} stdout := ProgressTemplate{}
err := json.Unmarshal([]byte(scan.Text()), &stdout) err := json.Unmarshal(scan.Bytes(), &stdout)
if err == nil { if err == nil {
p.Progress = DownloadProgress{ p.Progress = DownloadProgress{
Status: StatusDownloading, Status: StatusDownloading,
@@ -175,26 +175,49 @@ func (p *Process) Kill() error {
return err return err
} }
p.DB.Delete(p.Id)
return nil return nil
} }
// Returns the available format for this URL // Returns the available format for this URL
func (p *Process) GetFormatsSync() (DownloadFormats, error) { func (p *Process) GetFormatsSync() (DownloadFormats, error) {
cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.Url, "-J") cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.Url, "-J")
stdout, err := cmd.Output() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return DownloadFormats{}, err return DownloadFormats{}, err
} }
cmd.Wait()
info := DownloadFormats{URL: p.Url} info := DownloadFormats{URL: p.Url}
best := Format{} best := Format{}
json.Unmarshal(stdout, &info) var (
json.Unmarshal(stdout, &best) wg sync.WaitGroup
decodingError error
)
wg.Add(2)
err = cmd.Start()
if err != nil {
return DownloadFormats{}, err
}
go func() {
decodingError = json.NewDecoder(stdout).Decode(&info)
wg.Done()
}()
go func() {
decodingError = json.NewDecoder(stdout).Decode(&best)
wg.Done()
}()
wg.Wait()
cmd.Wait()
if decodingError != nil {
return DownloadFormats{}, err
}
info.Best = best info.Best = best
@@ -202,14 +225,17 @@ func (p *Process) GetFormatsSync() (DownloadFormats, error) {
} }
func (p *Process) SetPending() { func (p *Process) SetPending() {
p.Id = p.DB.Set(p) p.Progress.Status = StatusPending
}
func (p *Process) SetMetadata() error {
cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.Url, "-J") cmd := exec.Command(cfg.GetConfig().DownloaderPath, p.Url, "-J")
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdout, err := cmd.Output() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
log.Println("Cannot retrieve info for", p.Url) log.Println("Cannot retrieve info for", p.Url)
return err
} }
info := DownloadInfo{ info := DownloadInfo{
@@ -217,8 +243,20 @@ func (p *Process) SetPending() {
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
json.Unmarshal(stdout, &info) err = cmd.Start()
p.Info = info if err != nil {
return err
}
err = json.NewDecoder(stdout).Decode(&info)
if err != nil {
return err
}
p.Info = info
p.Progress.Status = StatusPending p.Progress.Status = StatusPending
err = cmd.Wait()
return err
} }

View File

@@ -7,7 +7,11 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
var upgrader websocket.Upgrader var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func WebSocket(w http.ResponseWriter, r *http.Request) { func WebSocket(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil) c, err := upgrader.Upgrade(w, r, nil)

View File

@@ -24,14 +24,6 @@ type Args struct {
Params []string Params []string
} }
type DownloadSpecificArgs struct {
Id string
URL string
Path string
Rename string
Params []string
}
// Dependency injection container. // Dependency injection container.
func Container(db *internal.MemoryDB, mq *internal.MessageQueue) *Service { func Container(db *internal.MemoryDB, mq *internal.MessageQueue) *Service {
return &Service{ return &Service{
@@ -42,11 +34,8 @@ func Container(db *internal.MemoryDB, mq *internal.MessageQueue) *Service {
// Exec spawns a Process. // Exec spawns a Process.
// The result of the execution is the newly spawned process Id. // The result of the execution is the newly spawned process Id.
func (s *Service) Exec(args DownloadSpecificArgs, result *string) error { func (s *Service) Exec(args internal.DownloadRequest, result *string) error {
log.Println("Sending new process to message queue", args.URL)
p := &internal.Process{ p := &internal.Process{
DB: s.db,
Url: args.URL, Url: args.URL,
Params: args.Params, Params: args.Params,
Output: internal.DownloadOutput{ Output: internal.DownloadOutput{
@@ -55,8 +44,22 @@ func (s *Service) Exec(args DownloadSpecificArgs, result *string) error {
}, },
} }
s.db.Set(p)
s.mq.Publish(p) s.mq.Publish(p)
*result = p.Id *result = p.Id
return nil
}
// Exec spawns a Process.
// The result of the execution is the newly spawned process Id.
func (s *Service) ExecPlaylist(args internal.DownloadRequest, result *string) error {
err := internal.PlaylistDetect(args, s.mq, s.db)
if err != nil {
return err
}
*result = ""
return nil return nil
} }
@@ -71,7 +74,7 @@ func (s *Service) Progess(args Args, progress *internal.DownloadProgress) error
return nil return nil
} }
// Progess retrieves the Progress of a specific Process given its Id // Progess retrieves available format for a given resource
func (s *Service) Formats(args Args, progress *internal.DownloadFormats) error { func (s *Service) Formats(args Args, progress *internal.DownloadFormats) error {
var err error var err error
p := internal.Process{Url: args.URL} p := internal.Process{Url: args.URL}
@@ -101,6 +104,7 @@ func (s *Service) Kill(args string, killed *string) error {
} }
if proc != nil { if proc != nil {
err = proc.Kill() err = proc.Kill()
s.db.Delete(proc.Id)
} }
s.db.Delete(proc.Id) s.db.Delete(proc.Id)
@@ -120,8 +124,10 @@ func (s *Service) KillAll(args NoArgs, killed *string) error {
} }
if proc != nil { if proc != nil {
proc.Kill() proc.Kill()
s.db.Delete(proc.Id)
} }
} }
s.mq.Empty()
return err return err
} }
@@ -142,7 +148,9 @@ func (s *Service) FreeSpace(args NoArgs, free *uint64) error {
// Return a flattned tree of the download directory // Return a flattned tree of the download directory
func (s *Service) DirectoryTree(args NoArgs, tree *[]string) error { func (s *Service) DirectoryTree(args NoArgs, tree *[]string) error {
dfsTree, err := sys.DirectoryTree() dfsTree, err := sys.DirectoryTree()
*tree = *dfsTree if dfsTree != nil {
*tree = *dfsTree
}
return err return err
} }

View File

@@ -14,6 +14,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/marcopeocchi/yt-dlp-web-ui/server/internal" "github.com/marcopeocchi/yt-dlp-web-ui/server/internal"
middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware" middlewares "github.com/marcopeocchi/yt-dlp-web-ui/server/middleware"
"github.com/marcopeocchi/yt-dlp-web-ui/server/rest" "github.com/marcopeocchi/yt-dlp-web-ui/server/rest"
@@ -54,7 +55,7 @@ func newServer(c serverConfig) *http.Server {
r := chi.NewRouter() r := chi.NewRouter()
r.Use(middlewares.CORS) r.Use(cors.AllowAll().Handler)
r.Use(middleware.Logger) r.Use(middleware.Logger)
sh := middlewares.NewSpaHandler("index.html", c.frontend) sh := middlewares.NewSpaHandler("index.html", c.frontend)