preset.go
⎇
Raw
1package service
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7 "vidarchive/internal/models"
8 "vidarchive/internal/repository"
9)
10
11type PresetService struct {
12 repo *repository.PresetRepository
13}
14
15func NewPresetService(repo *repository.PresetRepository) *PresetService {
16 return &PresetService{repo: repo}
17}
18
19func (s *PresetService) Create(p *models.Preset) error {
20 return s.repo.Save(p)
21}
22
23func (s *PresetService) GetByID(id int64) (*models.Preset, error) {
24 return s.repo.GetByID(id)
25}
26
27func (s *PresetService) GetAll() ([]*models.Preset, error) {
28 return s.repo.GetAll()
29}
30
31func (s *PresetService) GetDefault() (*models.Preset, error) {
32 return s.repo.GetDefault()
33}
34
35func (s *PresetService) Update(p *models.Preset) error {
36 return s.repo.Save(p)
37}
38
39func (s *PresetService) Delete(id int64) error {
40 return s.repo.Delete(id)
41}
42
43func (s *PresetService) BuildArgs(p *models.Preset, formatOverride, customFlags string) []string {
44 var args []string
45
46 // Format selection
47 if formatOverride != "" {
48 args = append(args, "-f", formatOverride)
49 } else if p.FormatMode == "custom" && p.CustomFormat != "" {
50 args = append(args, "-f", p.CustomFormat)
51 } else if p.FormatMode == "preset" && p.Format != "" {
52 if p.Quality != "" {
53 args = append(args, "-f", applyHeightCap(p.Format, p.Quality))
54 } else {
55 args = append(args, "-f", p.Format)
56 }
57 }
58 // If FormatMode is "default" or empty, don't pass -f (let yt-dlp choose)
59
60 // Audio extraction
61 if p.ExtractAudio {
62 args = append(args, "--extract-audio")
63 if p.AudioFormat != "" {
64 args = append(args, "--audio-format", p.AudioFormat)
65 }
66 }
67
68 // Subtitles
69 if p.EmbedSubs {
70 args = append(args, "--embed-subs")
71 if p.SubLangs != "" {
72 args = append(args, "--sub-langs", p.SubLangs)
73 }
74 }
75
76 // Thumbnail
77 if p.EmbedThumbnail {
78 args = append(args, "--embed-thumbnail")
79 }
80
81 // Metadata
82 if p.EmbedMetadata {
83 args = append(args, "--embed-metadata")
84 }
85
86 // Info JSON and comments. Comments are stored in the info JSON sidecar, so
87 // enabling comment collection implicitly enables sidecar writing.
88 if p.WriteInfoJSON || p.WriteComments {
89 args = append(args, "--write-info-json")
90 }
91 if p.WriteComments {
92 args = append(args, "--write-comments")
93 }
94 var commentArgs []string
95 commentSort := strings.TrimSpace(p.CommentSort)
96 if commentSort != "" {
97 commentArgs = append(commentArgs, "comment_sort="+commentSort)
98 }
99 if p.MaxComments > 0 {
100 commentArgs = append(commentArgs, "max_comments="+strconv.Itoa(p.MaxComments))
101 }
102 if len(commentArgs) > 0 {
103 args = append(args, "--extractor-args", "youtube:"+strings.Join(commentArgs, ","))
104 }
105 if strings.TrimSpace(p.CommentExtractorArgs) != "" {
106 args = append(args, "--extractor-args", strings.TrimSpace(p.CommentExtractorArgs))
107 }
108
109 // Custom flags
110 if p.CustomFlags != "" {
111 args = append(args, strings.Fields(p.CustomFlags)...)
112 }
113
114 if customFlags != "" {
115 args = append(args, strings.Fields(customFlags)...)
116 }
117
118 return args
119}
120
121// applyHeightCap adds a max-height filter to a format selector. For a combined
122// "video+audio" selector it binds the filter to the video stream only (e.g.
123// "bestvideo[height<=720]+bestaudio"), since applying it to the whole expression
124// would wrongly constrain the audio selector. It always appends an unfiltered
125// fallback so a missing capped variant still resolves.
126func applyHeightCap(format, maxHeight string) string {
127 parts := strings.Split(format, "+")
128 parts[0] = fmt.Sprintf("%s[height<=%s]", parts[0], maxHeight)
129 return strings.Join(parts, "+") + "/" + format
130}
131
132func (s *PresetService) EffectiveFlags(p *models.Preset, formatOverride, customFlags string) string {
133 args := s.BuildArgs(p, formatOverride, customFlags)
134 return strings.Join(args, " ")
135}
136