package service import ( "fmt" "strings" shellwords "github.com/mattn/go-shellwords" ) // splitFlags splits a custom-flags string like a POSIX shell would, so quoted // values such as --match-filter "duration > 60" stay one argument. func splitFlags(s string) ([]string, error) { return shellwords.Parse(s) } // reservedFlags are yt-dlp options VidArchive always sets itself; user custom // flags must not pass them (or a conflicting inverse). The value describes what // the option controls, for the failure message. var reservedFlags = map[string]string{ "-o": "the output template", "--output": "the output template", "-P": "the download path", "--paths": "the download path", "--cookies": "cookies (set these in Settings instead)", "--no-cookies": "cookies (set these in Settings instead)", "--newline": "progress output formatting (VidArchive sets this to stream logs)", "--write-playlist-metafiles": "playlist metadata files (VidArchive imports per-item metadata only)", "--no-write-playlist-metafiles": "playlist metadata files (VidArchive imports per-item metadata only)", // These options are blocked as a guard against accidental misuse. The list is // not a security boundary: yt-dlp accepts unambiguous option prefixes (e.g. // --exec-b) and --alias can define new options, and options such as // --ffmpeg-location or --plugin-dirs are not listed. Custom flags are // operator-controlled by design. "--exec": "running external commands (not permitted)", "--exec-before-download": "running external commands (not permitted)", "--postprocessor-args": "post-processor arguments (not permitted)", "--ppa": "post-processor arguments (not permitted)", "--downloader": "selecting an external downloader (not permitted)", "--external-downloader": "selecting an external downloader (not permitted)", "--downloader-args": "external downloader arguments (not permitted)", "--external-downloader-args": "external downloader arguments (not permitted)", } // reservedSubscriptionFlags are additionally reserved for subscription runs, // where VidArchive drives info-json writing and the refresh mode. var reservedSubscriptionFlags = map[string]string{ "--write-info-json": "info-json writing (needed to track item identity)", "--no-write-info-json": "info-json writing (needed to track item identity)", "--download-archive": "the download archive (managed by Skip mode)", "--no-download-archive": "the download archive (managed by Skip mode)", "--skip-download": "media downloading (managed by Metadata mode)", "--no-skip-download": "media downloading (managed by Metadata mode)", } // checkReservedFlags rejects custom flags that clash with options VidArchive // controls, naming the offender. It matches both "--flag" and "--flag=value". func checkReservedFlags(customFlags string, isSubscription bool) error { tokens, err := splitFlags(customFlags) if err != nil { return fmt.Errorf("invalid custom flags: %w", err) } for _, tok := range tokens { // Both "--flag value" and "--flag=value" name the same option. name, _, _ := strings.Cut(tok, "=") desc, ok := reservedFlags[name] if !ok && isSubscription { desc, ok = reservedSubscriptionFlags[name] } if ok { return fmt.Errorf("custom flag %q conflicts with VidArchive's handling of %s; remove it and try again", tok, desc) } } return nil }