package service import ( "fmt" "strings" "vidarchive/internal/models" "vidarchive/internal/repository" ) type PresetService struct { repo *repository.PresetRepository } func NewPresetService(repo *repository.PresetRepository) *PresetService { return &PresetService{repo: repo} } func (s *PresetService) Create(p *models.Preset) error { return s.repo.Save(p) } func (s *PresetService) GetByID(id int64) (*models.Preset, error) { return s.repo.GetByID(id) } func (s *PresetService) GetAll() ([]*models.Preset, error) { return s.repo.GetAll() } func (s *PresetService) GetDefault() (*models.Preset, error) { return s.repo.GetDefault() } func (s *PresetService) Update(p *models.Preset) error { return s.repo.Save(p) } func (s *PresetService) Delete(id int64) error { return s.repo.Delete(id) } func (s *PresetService) BuildArgs(p *models.Preset, formatOverride, customFlags string) []string { var args []string // Format selection if formatOverride != "" { args = append(args, "-f", formatOverride) } else if p.FormatMode == "custom" && p.CustomFormat != "" { args = append(args, "-f", p.CustomFormat) } else if p.FormatMode == "preset" && p.Format != "" { if p.Quality != "" { args = append(args, "-f", applyHeightCap(p.Format, p.Quality)) } else { args = append(args, "-f", p.Format) } } // If FormatMode is "default" or empty, don't pass -f (let yt-dlp choose) // Audio extraction if p.ExtractAudio { args = append(args, "--extract-audio") if p.AudioFormat != "" { args = append(args, "--audio-format", p.AudioFormat) } } // Subtitles if p.EmbedSubs { args = append(args, "--embed-subs") if p.SubLangs != "" { args = append(args, "--sub-langs", p.SubLangs) } } // Thumbnail if p.EmbedThumbnail { args = append(args, "--embed-thumbnail") } // Metadata if p.EmbedMetadata { args = append(args, "--embed-metadata") } // Info JSON if p.WriteInfoJSON { args = append(args, "--write-info-json") } // Custom flags if p.CustomFlags != "" { args = append(args, strings.Fields(p.CustomFlags)...) } if customFlags != "" { args = append(args, strings.Fields(customFlags)...) } return args } // applyHeightCap adds a max-height filter to a format selector. For a combined // "video+audio" selector it binds the filter to the video stream only (e.g. // "bestvideo[height<=720]+bestaudio"), since applying it to the whole expression // would wrongly constrain the audio selector. It always appends an unfiltered // fallback so a missing capped variant still resolves. func applyHeightCap(format, maxHeight string) string { parts := strings.Split(format, "+") parts[0] = fmt.Sprintf("%s[height<=%s]", parts[0], maxHeight) return strings.Join(parts, "+") + "/" + format } func (s *PresetService) EffectiveFlags(p *models.Preset, formatOverride, customFlags string) string { args := s.BuildArgs(p, formatOverride, customFlags) return strings.Join(args, " ") }