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
}