package cli import ( "bufio" "context" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "intake/internal/artwork" "intake/internal/audio" ) type coverChoice struct { Path string Placeholder bool Source artwork.Source Label string } func (a *App) chooseCover(ctx context.Context, info artwork.AlbumInfo, tracks []audio.Track, stagingDir string, excludeHashes []string, auto bool) (coverChoice, error) { if auto { local, _, _ := artwork.Discover(ctx, info, tracks, artwork.Options{ StagingDir: stagingDir, LocalCoverFolder: a.Cfg.CoverFolder, Offline: true, CacheDir: filepath.Join(a.Cfg.StateDir, "art-cache"), ExcludeHashes: excludeHashes, }) for _, c := range local { if c.Preferred || c.Source == artwork.SourceFolder || c.Source == artwork.SourceLocalFolder { a.logf("cover: %s (%s)", c.Label, c.Describe()) return coverChoice{Path: c.Path, Source: c.Source, Label: c.Label}, nil } } remote, _, _ := artwork.Discover(ctx, info, tracks, artwork.Options{ StagingDir: stagingDir, LocalCoverFolder: a.Cfg.CoverFolder, Offline: a.Cfg.Offline, CacheDir: filepath.Join(a.Cfg.StateDir, "art-cache"), ExcludeHashes: excludeHashes, Progress: func(msg string) { a.logf(" %s", msg) }, Sources: []artwork.Source{artwork.SourceCoverArt, artwork.SourceDeezer, artwork.SourceITunes}, }) for _, c := range remote { if c.ExactMatch() { a.logf("cover: %s (%s)", c.Label, c.Describe()) return coverChoice{Path: c.Path, Source: c.Source, Label: c.Label}, nil } } a.logf("cover: no confident match; will file with a placeholder") return a.placeholder(ctx, info) } candidates, warnings, err := artwork.Discover(ctx, info, tracks, artwork.Options{ StagingDir: stagingDir, LocalCoverFolder: a.Cfg.CoverFolder, Offline: a.Cfg.Offline, CacheDir: filepath.Join(a.Cfg.StateDir, "art-cache"), ExcludeHashes: excludeHashes, Progress: func(msg string) { a.logf(" %s", msg) }, }) if err != nil { return coverChoice{}, err } for _, w := range warnings { a.logf(" warning: %s", w) } if len(candidates) > 1 && candidates[0].Preferred && candidates[0].IsLowResolution() { a.logf(" note: the embedded art is only %s; larger options are listed below", candidates[0].Describe()) } placeholderPath, err := a.makePlaceholder(ctx, info) if err != nil { a.logf(" warning: could not generate a placeholder: %v", err) } a.logf("") a.logf("COVER ART") if len(candidates) == 0 { a.logf(" no candidates found from any source.") } for i, c := range candidates { a.logf(" [%d] %-16s %s", i+1, c.Source, c.Describe()) a.logf(" %s", c.Label) a.renderImage(c.Path) } if placeholderPath != "" { a.logf(" [0] placeholder a generated stand-in, recorded so you can replace it later") a.logf(" [n] none file the album with no cover at all") } else { a.logf(" [n] none file the album with no cover at all") } a.logf(" [p] pick a file use an image you already have") for attempt := 0; attempt < 5; attempt++ { fmt.Fprintf(a.Out, "choose a cover [1-%d, 0=placeholder, n=none, p=file]: ", len(candidates)) _ = os.Stdout.Sync() line, _ := bufio.NewReader(a.In).ReadString('\n') answer := strings.TrimSpace(line) switch { case answer == "": if len(candidates) > 0 { c := candidates[0] return coverChoice{Path: c.Path, Source: c.Source, Label: c.Label}, nil } case strings.EqualFold(answer, "n"): a.logf("cover: none") return coverChoice{}, nil case strings.EqualFold(answer, "p"): fmt.Fprintf(a.Out, "path to image: ") _ = os.Stdout.Sync() pathLine, _ := bufio.NewReader(a.In).ReadString('\n') path := strings.TrimSpace(pathLine) if path == "" { continue } path = strings.Trim(path, `"'`) if _, err := os.Stat(path); err != nil { a.logf(" cannot read %s: %v", path, err) continue } return coverChoice{Path: path, Source: artwork.SourceLocalFolder, Label: filepath.Base(path)}, nil case answer == "0" && placeholderPath != "": a.logf("cover: placeholder (recorded; use 'intake library re-cover' once you have real art)") return coverChoice{Path: placeholderPath, Placeholder: true, Source: artwork.SourcePlaceholder, Label: "placeholder"}, nil default: if n, err := strconv.Atoi(answer); err == nil && n >= 1 && n <= len(candidates) { c := candidates[n-1] return coverChoice{Path: c.Path, Source: c.Source, Label: c.Label}, nil } } a.logf(" please answer with a number between 1 and %d, or 0, n, p", len(candidates)) } return coverChoice{}, fmt.Errorf("no cover chosen after several attempts") } func (a *App) makePlaceholder(ctx context.Context, info artwork.AlbumInfo) (string, error) { dir := filepath.Join(a.Cfg.StateDir, "placeholders") path := filepath.Join(dir, "placeholder.jpg") if _, err := os.Stat(path); err == nil { return path, nil } if err := artwork.Placeholder(ctx, info, path, a.Cfg.LibraryArtPx); err != nil { return "", err } return path, nil } func (a *App) placeholder(ctx context.Context, info artwork.AlbumInfo) (coverChoice, error) { path, err := a.makePlaceholder(ctx, info) if err != nil { return coverChoice{}, nil } return coverChoice{Path: path, Placeholder: true, Source: artwork.SourcePlaceholder, Label: "placeholder"}, nil } func (a *App) renderImage(path string) { if path == "" || !a.ImagePreview { return } width := a.terminalColumns() - 6 if width > 44 { width = 44 } if width < 16 { return } cmd := exec.Command("chafa", "--format", "symbols", "--size", fmt.Sprintf("%dx24", width), path) cmd.Stdout = a.Out cmd.Stderr = nil _ = cmd.Run() } func (a *App) terminalColumns() int { if c, err := strconv.Atoi(os.Getenv("COLUMNS")); err == nil && c > 0 { return c } cmd := exec.Command("tput", "cols") var out strings.Builder cmd.Stdout = &out if err := cmd.Run(); err == nil { if c, err := strconv.Atoi(strings.TrimSpace(out.String())); err == nil && c > 0 { return c } } return 80 }