preset.go
⎇
Raw
1package service
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "vidarchive/internal/models"
9 "vidarchive/internal/repository"
10)
11
12// PresetService adds yt-dlp argument building on top of preset storage. The
13// repository is embedded rather than wrapped: every one of its methods is part
14// of this service's contract anyway.
15type PresetService struct {
16 *repository.PresetRepository
17}
18
19func NewPresetService(repo *repository.PresetRepository) *PresetService {
20 return &PresetService{PresetRepository: repo}
21}
22
23func (s *PresetService) BuildArgs(p *models.Preset, formatOverride, customFlags string) []string {
24 var args []string
25
26 if formatOverride != "" {
27 args = append(args, "-f", formatOverride)
28 } else if p.FormatMode == "custom" && p.CustomFormat != "" {
29 args = append(args, "-f", p.CustomFormat)
30 } else if p.FormatMode == "preset" && p.Format != "" {
31 if p.Quality != "" {
32 args = append(args, "-f", applyHeightCap(p.Format, p.Quality))
33 } else {
34 args = append(args, "-f", p.Format)
35 }
36 }
37 // A "default" or empty FormatMode passes no -f at all: yt-dlp chooses.
38
39 if p.ExtractAudio {
40 args = append(args, "--extract-audio")
41 if p.AudioFormat != "" {
42 args = append(args, "--audio-format", p.AudioFormat)
43 }
44 }
45
46 if p.EmbedSubs {
47 args = append(args, "--embed-subs")
48 if p.SubLangs != "" {
49 args = append(args, "--sub-langs", p.SubLangs)
50 }
51 }
52
53 if p.EmbedThumbnail {
54 args = append(args, "--embed-thumbnail")
55 }
56
57 if p.EmbedMetadata {
58 args = append(args, "--embed-metadata")
59 }
60
61 // Comments live in the info JSON sidecar, so collecting them implies writing it.
62 if p.WriteInfoJSON || p.WriteComments {
63 args = append(args, "--write-info-json")
64 }
65 if p.WriteComments {
66 args = append(args, "--write-comments")
67 }
68 var commentArgs []string
69 commentSort := strings.TrimSpace(p.CommentSort)
70 if commentSort != "" {
71 commentArgs = append(commentArgs, "comment_sort="+commentSort)
72 }
73 if p.MaxComments > 0 {
74 commentArgs = append(commentArgs, "max_comments="+strconv.Itoa(p.MaxComments))
75 }
76 // yt-dlp's syntax is IE_KEY:ARG1=VAL1,VAL2;ARG2=VAL. A second --extractor-args
77 // for the same key replaces the first, so every youtube argument has to go
78 // into one flag.
79 extra := strings.TrimSpace(p.CommentExtractorArgs)
80 if rest, ok := cutYoutubePrefix(extra); ok {
81 if rest != "" {
82 commentArgs = append(commentArgs, rest)
83 }
84 extra = ""
85 }
86 if len(commentArgs) > 0 {
87 args = append(args, "--extractor-args", "youtube:"+strings.Join(commentArgs, ";"))
88 }
89 if extra != "" {
90 args = append(args, "--extractor-args", extra)
91 }
92
93 args = append(args, splitCustomFlags(p.CustomFlags)...)
94 args = append(args, splitCustomFlags(customFlags)...)
95
96 return args
97}
98
99// cutYoutubePrefix strips a leading "youtube:", any case. The bool reports
100// whether the value targeted the youtube extractor at all.
101func cutYoutubePrefix(s string) (string, bool) {
102 if len(s) < len("youtube:") || !strings.EqualFold(s[:len("youtube:")], "youtube:") {
103 return s, false
104 }
105 return strings.TrimSpace(s[len("youtube:"):]), true
106}
107
108// splitCustomFlags falls back to whitespace splitting on malformed quoting,
109// because BuildArgs cannot report an error. Only the EffectiveFlags display
110// reaches that fallback; the download path rejects such input beforehand.
111func splitCustomFlags(s string) []string {
112 if strings.TrimSpace(s) == "" {
113 return nil
114 }
115 fields, err := splitFlags(s)
116 if err != nil {
117 return strings.Fields(s)
118 }
119 return fields
120}
121
122// applyHeightCap binds the filter to the video stream only, since a combined
123// "video+audio" selector would otherwise constrain the audio too. The unfiltered
124// fallback keeps a missing capped variant resolvable.
125func applyHeightCap(format, maxHeight string) string {
126 parts := strings.Split(format, "+")
127 parts[0] = fmt.Sprintf("%s[height<=%s]", parts[0], maxHeight)
128 return strings.Join(parts, "+") + "/" + format
129}
130
131func (s *PresetService) EffectiveFlags(p *models.Preset, formatOverride, customFlags string) string {
132 args := s.BuildArgs(p, formatOverride, customFlags)
133 return strings.Join(args, " ")
134}
135