diff --git a/Makefile b/Makefile index ef288ee..0c803b9 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ fe: cd frontend && pnpm install && pnpm build dev: - ( cd frontend && pnpm install && pnpm dev ) + cd frontend && pnpm install && pnpm dev all: fe CGO_ENABLED=0 go build -o yt-dlp-webui main.go diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 409005f..d4ae3aa 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -59,7 +59,7 @@ importers: version: 18.3.3 '@types/react-dom': specifier: ^18.2.18 - version: 18.2.18 + version: 18.3.1 '@types/react-helmet': specifier: ^6.1.11 version: 6.1.11 @@ -689,8 +689,8 @@ packages: '@types/prop-types@15.7.11': resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} - '@types/react-dom@18.2.18': - resolution: {integrity: sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==} + '@types/react-dom@18.3.1': + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} '@types/react-helmet@6.1.11': resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==} @@ -1711,7 +1711,7 @@ snapshots: '@types/prop-types@15.7.11': {} - '@types/react-dom@18.2.18': + '@types/react-dom@18.3.1': dependencies: '@types/react': 18.3.3 diff --git a/frontend/src/components/DownloadDialog.tsx b/frontend/src/components/DownloadDialog.tsx index 6e402f2..5151184 100644 --- a/frontend/src/components/DownloadDialog.tsx +++ b/frontend/src/components/DownloadDialog.tsx @@ -40,6 +40,8 @@ import { useRPC } from '../hooks/useRPC' import type { DLMetadata } from '../types' import { toFormatArgs } from '../utils' import ExtraDownloadOptions from './ExtraDownloadOptions' +import { useToast } from '../hooks/toast' +import LoadingBackdrop from './LoadingBackdrop' const Transition = forwardRef(function Transition( props: TransitionProps & { @@ -67,6 +69,7 @@ const DownloadDialog: FC = ({ open, onClose, onDownloadStart }) => { const [pickedVideoFormat, setPickedVideoFormat] = useState('') const [pickedAudioFormat, setPickedAudioFormat] = useState('') const [pickedBestFormat, setPickedBestFormat] = useState('') + const [isFormatsLoading, setIsFormatsLoading] = useState(false) const [customArgs, setCustomArgs] = useRecoilState(customArgsState) @@ -82,6 +85,7 @@ const DownloadDialog: FC = ({ open, onClose, onDownloadStart }) => { const { i18n } = useI18n() const { client } = useRPC() + const { pushMessage } = useToast() const urlInputRef = useRef(null) const customFilenameInputRef = useRef(null) @@ -129,11 +133,28 @@ const DownloadDialog: FC = ({ open, onClose, onDownloadStart }) => { setPickedVideoFormat('') setPickedBestFormat('') + + if (isPlaylist) { + pushMessage('Format selection on playlist is not supported', 'warning') + resetInput() + onClose() + return + } + + setIsFormatsLoading(true) + client.formats(url) ?.then(formats => { + if (formats.result._type === 'playlist') { + pushMessage('Format selection on playlist is not supported. Downloading as playlist.', 'info') + resetInput() + onClose() + return + } setDownloadFormats(formats.result) resetInput() }) + .then(() => setIsFormatsLoading(false)) } const handleUrlChange = (e: React.ChangeEvent) => { @@ -175,10 +196,7 @@ const DownloadDialog: FC = ({ open, onClose, onDownloadStart }) => { onClose={onClose} TransitionComponent={Transition} > - theme.zIndex.drawer + 1 }} - open={isPending} - /> + + _type: string best: DLFormat thumbnail: string title: string + entries: Array } export type DLFormat = { diff --git a/server/formats/parser.go b/server/formats/parser.go new file mode 100644 index 0000000..580ccc4 --- /dev/null +++ b/server/formats/parser.go @@ -0,0 +1,56 @@ +package formats + +import ( + "encoding/json" + "log/slog" + "os/exec" + "sync" + + "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config" +) + +func ParseURL(url string) (*Metadata, error) { + cmd := exec.Command(config.Instance().DownloaderPath, url, "-J") + + stdout, err := cmd.Output() + if err != nil { + slog.Error("failed to retrieve metadata", slog.String("err", err.Error())) + return nil, err + } + + slog.Info( + "retrieving metadata", + slog.String("caller", "getFormats"), + slog.String("url", url), + ) + + info := &Metadata{URL: url} + best := &Format{} + + var ( + wg sync.WaitGroup + decodingError error + ) + + wg.Add(2) + + go func() { + decodingError = json.Unmarshal(stdout, &info) + wg.Done() + }() + + go func() { + decodingError = json.Unmarshal(stdout, &best) + wg.Done() + }() + + wg.Wait() + + if decodingError != nil { + return nil, err + } + + info.Best = *best + + return info, nil +} diff --git a/server/formats/types.go b/server/formats/types.go new file mode 100644 index 0000000..864d86d --- /dev/null +++ b/server/formats/types.go @@ -0,0 +1,28 @@ +package formats + +// Used to deser the formats in the -J output +type Metadata struct { + Type string `json:"_type"` + Formats []Format `json:"formats"` + Best Format `json:"best"` + Thumbnail string `json:"thumbnail"` + Title string `json:"title"` + URL string `json:"url"` + Entries []Metadata `json:"entries"` // populated if url is playlist +} + +func (m *Metadata) IsPlaylist() bool { + return m.Type == "playlist" +} + +// A skimmed yt-dlp format node +type Format struct { + Format_id string `json:"format_id"` + Format_note string `json:"format_note"` + FPS float32 `json:"fps"` + Resolution string `json:"resolution"` + VCodec string `json:"vcodec"` + ACodec string `json:"acodec"` + Size float32 `json:"filesize_approx"` + Language string `json:"language"` +} diff --git a/server/internal/common_types.go b/server/internal/common_types.go index 32dbcd2..f6b0b2a 100644 --- a/server/internal/common_types.go +++ b/server/internal/common_types.go @@ -10,7 +10,6 @@ type ProgressTemplate struct { Eta float32 `json:"eta"` } - type PostprocessTemplate struct { FilePath string `json:"filepath"` } @@ -45,27 +44,6 @@ type DownloadInfo struct { CreatedAt time.Time `json:"created_at"` } -// Used to deser the formats in the -J output -type DownloadFormats struct { - Formats []Format `json:"formats"` - Best Format `json:"best"` - Thumbnail string `json:"thumbnail"` - Title string `json:"title"` - URL string `json:"url"` -} - -// A skimmed yt-dlp format node -type Format struct { - Format_id string `json:"format_id"` - Format_note string `json:"format_note"` - FPS float32 `json:"fps"` - Resolution string `json:"resolution"` - VCodec string `json:"vcodec"` - ACodec string `json:"acodec"` - Size float32 `json:"filesize_approx"` - Language string `json:"language"` -} - // struct representing the response sent to the client // as JSON-RPC result field type ProcessResponse struct { diff --git a/server/internal/process.go b/server/internal/process.go index 685a860..69ccf05 100644 --- a/server/internal/process.go +++ b/server/internal/process.go @@ -11,7 +11,6 @@ import ( "log/slog" "regexp" "slices" - "sync" "syscall" "os" @@ -261,54 +260,6 @@ func (p *Process) Kill() error { return nil } -// Returns the available format for this URL -// -// TODO: Move out from process.go -func (p *Process) GetFormats() (DownloadFormats, error) { - cmd := exec.Command(config.Instance().DownloaderPath, p.Url, "-J") - - stdout, err := cmd.Output() - if err != nil { - slog.Error("failed to retrieve metadata", slog.String("err", err.Error())) - return DownloadFormats{}, err - } - - slog.Info( - "retrieving metadata", - slog.String("caller", "getFormats"), - slog.String("url", p.Url), - ) - - info := DownloadFormats{URL: p.Url} - best := Format{} - - var ( - wg sync.WaitGroup - decodingError error - ) - - wg.Add(2) - - go func() { - decodingError = json.Unmarshal(stdout, &info) - wg.Done() - }() - go func() { - decodingError = json.Unmarshal(stdout, &best) - wg.Done() - }() - - wg.Wait() - - if decodingError != nil { - return DownloadFormats{}, err - } - - info.Best = best - - return info, nil -} - func (p *Process) GetFileName(o *DownloadOutput) error { cmd := exec.Command( config.Instance().DownloaderPath, diff --git a/server/rpc/service.go b/server/rpc/service.go index ee075c2..5eed8ec 100644 --- a/server/rpc/service.go +++ b/server/rpc/service.go @@ -4,6 +4,7 @@ import ( "errors" "log/slog" + "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/formats" "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal" "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal/livestream" "github.com/marcopeocchi/yt-dlp-web-ui/v3/server/sys" @@ -21,12 +22,6 @@ type Pending []string type NoArgs struct{} -type Args struct { - Id string - URL string - Params []string -} - // Exec spawns a Process. // The result of the execution is the newly spawned process Id. func (s *Service) Exec(args internal.DownloadRequest, result *string) error { @@ -91,7 +86,7 @@ func (s *Service) KillAllLivestream(args NoArgs, result *struct{}) error { } // Progess retrieves the Progress of a specific Process given its Id -func (s *Service) Progess(args Args, progress *internal.DownloadProgress) error { +func (s *Service) Progess(args internal.DownloadRequest, progress *internal.DownloadProgress) error { proc, err := s.db.Get(args.Id) if err != nil { return err @@ -102,13 +97,20 @@ func (s *Service) Progess(args Args, progress *internal.DownloadProgress) error } // Progess retrieves available format for a given resource -func (s *Service) Formats(args Args, meta *internal.DownloadFormats) error { - var ( - err error - p = internal.Process{Url: args.URL} - ) - *meta, err = p.GetFormats() - return err +func (s *Service) Formats(args internal.DownloadRequest, meta *formats.Metadata) error { + var err error + + metadata, err := formats.ParseURL(args.URL) + if err != nil && metadata == nil { + return err + } + + if metadata.IsPlaylist() { + go internal.PlaylistDetect(args, s.mq, s.db) + } + + *meta = *metadata + return nil } // Pending retrieves a slice of all Pending/Running processes ids